Compare commits
246 Commits
085333db29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59526b20d | ||
|
|
008cd7ae86 | ||
|
|
0f7220d0b1 | ||
|
|
0e572f8cbe | ||
|
|
cc89fd4bef | ||
|
|
42d0652e48 | ||
|
|
ddd8ca49d8 | ||
|
|
8aeefccc3b | ||
|
|
2c453a4a6b | ||
|
|
cfab56f63d | ||
|
|
ab3e8d14e5 | ||
|
|
5993f79e7a | ||
|
|
b1365dccbb | ||
|
|
511146da74 | ||
|
|
3365f1dd2a | ||
|
|
c6ffdd10f7 | ||
|
|
530855e26e | ||
|
|
e94ae4350d | ||
|
|
ff68fb72c4 | ||
|
|
90e1c0496a | ||
|
|
a02fe1f008 | ||
|
|
7fc9b55b59 | ||
|
|
d854200708 | ||
|
|
7fd8e4dda8 | ||
|
|
0e39ca9a0f | ||
|
|
79fe05c630 | ||
|
|
e3c024d5af | ||
|
|
783dd175e8 | ||
|
|
cab5428d0b | ||
|
|
478a7ffc96 | ||
|
|
677e4c674e | ||
|
|
d3aad6e7ef | ||
|
|
44b40d5a46 | ||
|
|
4cc2aace6b | ||
|
|
27d42c0a3a | ||
|
|
7417736daf | ||
|
|
267711fca6 | ||
|
|
8ce15441bf | ||
|
|
1500f01875 | ||
|
|
5f13583e41 | ||
|
|
dee4991be7 | ||
|
|
d7fa5f925e | ||
|
|
e28ed7bdb5 | ||
|
|
553f132184 | ||
|
|
f64c923db6 | ||
|
|
71f2ee7c97 | ||
|
|
e6c90c219b | ||
|
|
b6d749f781 | ||
|
|
39dcdd7fb3 | ||
|
|
afbea926a2 | ||
|
|
1dd4d18927 | ||
|
|
d2eebf1f94 | ||
|
|
7732498875 | ||
|
|
14881803df | ||
|
|
3232e42251 | ||
|
|
9c121d2dc2 | ||
|
|
71d5922409 | ||
|
|
5c315c477f | ||
|
|
776dea2584 | ||
|
|
4205639de3 | ||
|
|
4df8f97a41 | ||
|
|
f93a0f8b35 | ||
|
|
b3346d4cac | ||
|
|
1b9d9664b2 | ||
|
|
9a92940dab | ||
|
|
5070785a50 | ||
|
|
70c381114b | ||
|
|
de9f2c853d | ||
|
|
1e9e247dec | ||
|
|
7b907ea683 | ||
|
|
e96c37aac5 | ||
|
|
1f10e7c519 | ||
|
|
2461e98fb0 | ||
|
|
3f1b474fdd | ||
|
|
f8f5017436 | ||
|
|
a022b8c174 | ||
|
|
09a10ff830 | ||
|
|
8be215761d | ||
|
|
e2c1147d75 | ||
|
|
54a77c2e08 | ||
|
|
d119869750 | ||
|
|
712370cad3 | ||
|
|
229bdc96bf | ||
|
|
c1421db72c | ||
|
|
68ac5ec281 | ||
|
|
6dce418728 | ||
|
|
b2942c1c9d | ||
|
|
41b07a8951 | ||
|
|
9bc6f32b96 | ||
|
|
8e29953a95 | ||
|
|
e26a3333c4 | ||
|
|
44850d5913 | ||
|
|
a294a94075 | ||
|
|
cb0e1eb2b1 | ||
|
|
95ea6336b7 | ||
|
|
7b4c9a0b1c | ||
|
|
cc791501c9 | ||
|
|
feda7d90f2 | ||
|
|
ff4fbbfab1 | ||
|
|
5d16021557 | ||
|
|
3500940d1c | ||
|
|
f841a35501 | ||
|
|
78da376c5b | ||
|
|
7e5a46a9bf | ||
|
|
deb6f5f36c | ||
|
|
26daf5fed5 | ||
|
|
9deda3147e | ||
|
|
b50d2a9a93 | ||
|
|
f92b62e55b | ||
|
|
86e14a875d | ||
|
|
b0624422b8 | ||
|
|
162e908c1c | ||
|
|
1bce855b3a | ||
|
|
ba72d4fb74 | ||
|
|
e2f4b255ff | ||
|
|
60ef98283f | ||
|
|
c6419c6c34 | ||
|
|
f46c864bbc | ||
|
|
545314e905 | ||
|
|
d17c8a341d | ||
|
|
9582e7b900 | ||
|
|
7e0821c96b | ||
|
|
3450bac99c | ||
|
|
1dfa49191f | ||
|
|
5cbd6d06b1 | ||
|
|
2b66f76dd9 | ||
|
|
204b7aed04 | ||
|
|
f5a5639e97 | ||
|
|
641a2134cb | ||
|
|
1118a691b9 | ||
|
|
360bb59a4e | ||
|
|
f7030bbabe | ||
|
|
f715c6125d | ||
|
|
e9f0f6c133 | ||
|
|
ae635b9c16 | ||
|
|
c5b8860605 | ||
|
|
ebb2283646 | ||
|
|
160c9dafb2 | ||
|
|
ee338c0e49 | ||
|
|
fe9322c098 | ||
|
|
3a1d83f20c | ||
|
|
e649236e39 | ||
|
|
8f6f06caf0 | ||
|
|
62503191d4 | ||
|
|
86dfb0d859 | ||
|
|
f504a5d597 | ||
|
|
3cc5f63610 | ||
|
|
e17f0cdce0 | ||
|
|
d192bcae2d | ||
|
|
07ab648143 | ||
|
|
e063df5cbe | ||
|
|
56be4b76c0 | ||
|
|
5d06d97737 | ||
|
|
49713d957d | ||
|
|
5fcd55be43 | ||
|
|
77e6f8d3e8 | ||
|
|
3d2ccd620a | ||
|
|
edbf22ac5b | ||
|
|
9d663e4f2b | ||
|
|
153914d5d2 | ||
|
|
2272db7f91 | ||
|
|
b3c8e8e210 | ||
|
|
02f3e82987 | ||
|
|
c3b2c60362 | ||
|
|
13534498fa | ||
|
|
3fb4fb92c6 | ||
|
|
6d9d69dc10 | ||
|
|
ac5d436a36 | ||
|
|
3ff8e4fc40 | ||
|
|
10fc78e81d | ||
|
|
d39cea2c01 | ||
|
|
d38231b52c | ||
|
|
7fee9e12d4 | ||
|
|
0421b2bc00 | ||
|
|
09015b4244 | ||
|
|
a3b820cea0 | ||
|
|
84adfeafb4 | ||
|
|
b8e3732ef8 | ||
|
|
8bbfd46ada | ||
|
|
6d13965c76 | ||
|
|
1328e4983e | ||
|
|
02b3636e10 | ||
|
|
0c89c48e68 | ||
|
|
7e45049e94 | ||
|
|
1e801b33a5 | ||
|
|
c6caeefb5f | ||
|
|
b1990334b9 | ||
|
|
3187a6e7b0 | ||
|
|
01293b0102 | ||
|
|
0d625f1727 | ||
|
|
db0e80a559 | ||
|
|
9b3898e43c | ||
|
|
dba9eb8692 | ||
|
|
734b8f9463 | ||
|
|
6b3aee458a | ||
|
|
53f0745faf | ||
|
|
9a78bc7c4b | ||
|
|
ee11a989a0 | ||
|
|
9aad42655e | ||
|
|
ca33a29317 | ||
|
|
4ebff4dc17 | ||
|
|
3d9bca099c | ||
|
|
2b83c45e97 | ||
|
|
f35db4b1a1 | ||
|
|
c52d4b60f9 | ||
|
|
b2591da428 | ||
|
|
5001292616 | ||
|
|
8f461d4dba | ||
|
|
ed4c4e1b40 | ||
|
|
4ed9ea39a0 | ||
|
|
0749c733a4 | ||
|
|
966c73dda9 | ||
|
|
34f22229bb | ||
|
|
05b91284fa | ||
|
|
c531ae2caf | ||
|
|
028da5a9f0 | ||
|
|
10d8ee015c | ||
|
|
c9a7619737 | ||
|
|
a2c86247b6 | ||
|
|
8a96951b50 | ||
|
|
71e120bf20 | ||
|
|
39032570e3 | ||
|
|
9e8f8e8077 | ||
|
|
e76be33743 | ||
|
|
6cbcf9d95f | ||
|
|
31a96aaf60 | ||
|
|
8a1ff52a61 | ||
|
|
84c598bf52 | ||
|
|
291e79c41f | ||
|
|
3b823420e6 | ||
|
|
674c4d0b69 | ||
|
|
9f3facbb3f | ||
|
|
07604cc9fa | ||
|
|
82223676a6 | ||
|
|
207ef6266a | ||
|
|
02837c7b73 | ||
|
|
25b658acce | ||
|
|
0f0c102ded | ||
|
|
26eb7b8ce7 | ||
|
|
0dd2bce5d1 | ||
|
|
cf6d72385e | ||
|
|
1a86061680 | ||
|
|
e13deb0720 | ||
|
|
21072139f7 | ||
|
|
1878b2a8c7 | ||
|
|
6563ca23c7 |
9
.cursor/rules/legacy-cpp-workers.mdc
Normal file
9
.cursor/rules/legacy-cpp-workers.mdc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Legacy C++ (`src/`)
|
||||||
|
|
||||||
|
- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt.
|
||||||
|
- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`.
|
||||||
121
.gitea/workflows/deploy.yml
Normal file
121
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
name: Deploy to production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Detect vocab course changes
|
||||||
|
id: vocab_course_changes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="${{ gitea.event.before }}"
|
||||||
|
HEAD="${{ gitea.sha }}"
|
||||||
|
|
||||||
|
if [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
|
||||||
|
BASE="HEAD~1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git diff --name-only "$BASE" "$HEAD" > changed-files.txt
|
||||||
|
cat changed-files.txt
|
||||||
|
COMMIT_MESSAGE="$(git log -1 --pretty=%B "$HEAD" || true)"
|
||||||
|
|
||||||
|
if echo "$COMMIT_MESSAGE" | grep -qi '\[force-deploy\]'; then
|
||||||
|
echo "force_deploy=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "force_deploy=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -E '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))' changed-files.txt; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -E '^frontend/' changed-files.txt >/dev/null; then
|
||||||
|
echo "frontend_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "frontend_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -E '^backend/' changed-files.txt \
|
||||||
|
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab)$' >/dev/null; then
|
||||||
|
echo "backend_app_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "backend_app_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# App-Code-Änderungen, die einen echten Deploy benötigen
|
||||||
|
# (Frontend oder Backend außerhalb reiner Kurs-/Dokument-Sync-Dateien)
|
||||||
|
if grep -E '^(frontend/|backend/)' changed-files.txt \
|
||||||
|
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))'; then
|
||||||
|
echo "app_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "app_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan -p "${{ secrets.PROD_PORT }}" "${{ secrets.PROD_HOST }}" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Test SSH connection
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/id_ed25519 \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-p "${{ secrets.PROD_PORT }}" \
|
||||||
|
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
||||||
|
"echo SSH OK"
|
||||||
|
|
||||||
|
- name: Run deployment script
|
||||||
|
if: steps.vocab_course_changes.outputs.app_changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
|
||||||
|
run: |
|
||||||
|
DEPLOY_FLAGS=""
|
||||||
|
if [ "${{ steps.vocab_course_changes.outputs.force_deploy }}" = "true" ]; then
|
||||||
|
DEPLOY_FLAGS=""
|
||||||
|
elif [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" != "true" ]; then
|
||||||
|
DEPLOY_FLAGS="--skip-frontend"
|
||||||
|
elif [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" != "true" ]; then
|
||||||
|
DEPLOY_FLAGS="--skip-backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_TARGET="${{ secrets.PROD_DEPLOY_TARGET }}"
|
||||||
|
if [ -z "$DEPLOY_TARGET" ]; then
|
||||||
|
DEPLOY_TARGET="/opt/yourpart-green"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploy-Flags: ${DEPLOY_FLAGS:-<none>}"
|
||||||
|
echo "Deploy-Target: $DEPLOY_TARGET"
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/id_ed25519 \
|
||||||
|
-p "${{ secrets.PROD_PORT }}" \
|
||||||
|
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
||||||
|
"/home/tsschulz/deploy-yourpart-bluegreen.sh ${DEPLOY_TARGET} ${DEPLOY_FLAGS}"
|
||||||
|
|
||||||
|
- name: Skip full deployment (no app changes)
|
||||||
|
if: steps.vocab_course_changes.outputs.app_changed != 'true' && steps.vocab_course_changes.outputs.force_deploy != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Kein Full-Deploy: Es wurden keine Frontend/Backend-App-Dateien geändert."
|
||||||
|
|
||||||
|
- name: Sync vocab course content
|
||||||
|
if: steps.vocab_course_changes.outputs.changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/id_ed25519 \
|
||||||
|
-p "${{ secrets.PROD_PORT }}" \
|
||||||
|
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
||||||
|
"cd /opt/yourpart && npm --prefix backend run sync:vocab-courses"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ frontend/node_modules
|
|||||||
frontend/node_modules/*
|
frontend/node_modules/*
|
||||||
frontend/dist
|
frontend/dist
|
||||||
frontend/dist/*
|
frontend/dist/*
|
||||||
|
frontend/scripts/.i18n-de-fr-cache.json
|
||||||
|
frontend/scripts/.falukant-fr-smooth-cache.json
|
||||||
|
frontend/ceb-locale-audit-report.json
|
||||||
frontedtree.txt
|
frontedtree.txt
|
||||||
backend/dist/
|
backend/dist/
|
||||||
backend/data/model-cache
|
backend/data/model-cache
|
||||||
|
|||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## zum testen des push
|
||||||
|
|
||||||
|
Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md).
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
This project now supports a per-region sales tax (`tax_percent`) for Falukant.
|
||||||
|
|
||||||
Migration
|
Migration
|
||||||
- A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`.
|
- A SQL migration was added: `backend/migrations-archive/20260101000000-add-tax-percent-to-region.cjs`.
|
||||||
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
|
||||||
|
|
||||||
Runtime configuration
|
Runtime configuration
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import vocabRouter from './routers/vocabRouter.js';
|
|||||||
import dashboardRouter from './routers/dashboardRouter.js';
|
import dashboardRouter from './routers/dashboardRouter.js';
|
||||||
import newsRouter from './routers/newsRouter.js';
|
import newsRouter from './routers/newsRouter.js';
|
||||||
import calendarRouter from './routers/calendarRouter.js';
|
import calendarRouter from './routers/calendarRouter.js';
|
||||||
|
import moderationRouter from './routers/moderationRouter.js';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import './jobs/sessionCleanup.js';
|
import './jobs/sessionCleanup.js';
|
||||||
|
|
||||||
@@ -83,7 +84,12 @@ const corsOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.options('*', cors(corsOptions));
|
app.use((req, res, next) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return cors(corsOptions)(req, res, next);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
app.use(express.json()); // To handle JSON request bodies
|
app.use(express.json()); // To handle JSON request bodies
|
||||||
|
|
||||||
app.use('/api/chat', chatRouter);
|
app.use('/api/chat', chatRouter);
|
||||||
@@ -100,6 +106,7 @@ app.use('/api/contact', contactRouter);
|
|||||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||||
app.use('/api/vocab', vocabRouter);
|
app.use('/api/vocab', vocabRouter);
|
||||||
app.use('/api/forum', forumRouter);
|
app.use('/api/forum', forumRouter);
|
||||||
|
app.use('/api/moderation', moderationRouter);
|
||||||
app.use('/api/falukant', falukantRouter);
|
app.use('/api/falukant', falukantRouter);
|
||||||
app.use('/api/friendships', friendshipRouter);
|
app.use('/api/friendships', friendshipRouter);
|
||||||
app.use('/api/models', modelsProxyRouter);
|
app.use('/api/models', modelsProxyRouter);
|
||||||
@@ -124,6 +131,11 @@ app.get(/^\/(?!api\/).*/, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fallback 404 for unknown API routes
|
// Fallback 404 for unknown API routes
|
||||||
app.use('/api/*', (req, res) => res.status(404).send('404 Not Found'));
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/api/')) {
|
||||||
|
return res.status(404).send('404 Not Found');
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
33
backend/config/sequelize-cli.cjs
Normal file
33
backend/config/sequelize-cli.cjs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
|
||||||
|
const envPath = process.env.SEQUELIZE_ENV_FILE
|
||||||
|
? path.resolve(process.cwd(), process.env.SEQUELIZE_ENV_FILE)
|
||||||
|
: path.resolve(process.cwd(), '.env');
|
||||||
|
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
|
||||||
|
const dialectOptions = {};
|
||||||
|
if (process.env.DB_SSL === '1' || process.env.PGSSLMODE === 'require') {
|
||||||
|
dialectOptions.ssl = process.env.DB_SSL_REJECT_UNAUTHORIZED === '0'
|
||||||
|
? { rejectUnauthorized: false }
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pg/SCRAM: password muss ein String sein; bei fehlender .env sonst undefined-Fallen vermeiden
|
||||||
|
const shared = {
|
||||||
|
username: process.env.DB_USER != null ? String(process.env.DB_USER) : undefined,
|
||||||
|
password: process.env.DB_PASS != null ? String(process.env.DB_PASS) : '',
|
||||||
|
database: process.env.DB_NAME != null ? String(process.env.DB_NAME) : undefined,
|
||||||
|
host: process.env.DB_HOST || '127.0.0.1',
|
||||||
|
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
|
dialect: 'postgres',
|
||||||
|
logging: false,
|
||||||
|
dialectOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
development: shared,
|
||||||
|
test: shared,
|
||||||
|
production: shared
|
||||||
|
};
|
||||||
@@ -13,6 +13,11 @@ class AdminController {
|
|||||||
this.searchUser = this.searchUser.bind(this);
|
this.searchUser = this.searchUser.bind(this);
|
||||||
this.getFalukantUserById = this.getFalukantUserById.bind(this);
|
this.getFalukantUserById = this.getFalukantUserById.bind(this);
|
||||||
this.changeFalukantUser = this.changeFalukantUser.bind(this);
|
this.changeFalukantUser = this.changeFalukantUser.bind(this);
|
||||||
|
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
|
||||||
|
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
|
||||||
|
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
|
||||||
|
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
|
||||||
|
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
|
||||||
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
|
||||||
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
this.updateFalukantStock = this.updateFalukantStock.bind(this);
|
||||||
this.addFalukantStock = this.addFalukantStock.bind(this);
|
this.addFalukantStock = this.addFalukantStock.bind(this);
|
||||||
@@ -29,6 +34,10 @@ class AdminController {
|
|||||||
this.getUser = this.getUser.bind(this);
|
this.getUser = this.getUser.bind(this);
|
||||||
this.getUsers = this.getUsers.bind(this);
|
this.getUsers = this.getUsers.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
|
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
|
||||||
|
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
|
||||||
|
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
|
||||||
|
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
|
||||||
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
||||||
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
|
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
|
||||||
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
|
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
|
||||||
@@ -45,6 +54,9 @@ class AdminController {
|
|||||||
// Statistics
|
// Statistics
|
||||||
this.getUserStatistics = this.getUserStatistics.bind(this);
|
this.getUserStatistics = this.getUserStatistics.bind(this);
|
||||||
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
this.getFalukantRegions = this.getFalukantRegions.bind(this);
|
||||||
|
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
|
||||||
|
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
|
||||||
|
this.createFalukantRegion = this.createFalukantRegion.bind(this);
|
||||||
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
|
||||||
this.getRegionDistances = this.getRegionDistances.bind(this);
|
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||||
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
||||||
@@ -125,6 +137,77 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resetUserVocabLessonProgress(req, res) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
lessonId: Joi.number().integer().positive().required()
|
||||||
|
});
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await AdminService.adminResetUserVocabLessonProgress(requester, id, value.lessonId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message === 'noaccess' ? 403 : (err.message === 'lessonnotfound' ? 404 : 500);
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markUserVocabLessonsCompleteThrough(req, res) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
courseId: Joi.number().integer().positive().required(),
|
||||||
|
throughLessonNumber: Joi.number().integer().positive().required()
|
||||||
|
});
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
|
||||||
|
requester,
|
||||||
|
id,
|
||||||
|
value.courseId,
|
||||||
|
value.throughLessonNumber
|
||||||
|
);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
let status = 500;
|
||||||
|
if (err.message === 'noaccess') status = 403;
|
||||||
|
else if (err.message === 'notenrolled') status = 403;
|
||||||
|
else if (err.message === 'badrequest') status = 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVocabCourses(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await AdminService.adminListUserEnrolledVocabCourses(requester, id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message === 'noaccess' ? 403 : (err.message === 'notfound' ? 404 : 500);
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVocabCourseForAdmin(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: requester } = req.headers;
|
||||||
|
const { courseId } = req.params;
|
||||||
|
const result = await AdminService.adminGetVocabCourseWithLessons(requester, courseId);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message === 'noaccess' ? 403 : (err.message === 'coursenotfound' ? 404 : 500);
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAdultVerificationRequests(req, res) {
|
async getAdultVerificationRequests(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: requester } = req.headers;
|
const { userid: requester } = req.headers;
|
||||||
@@ -372,6 +455,77 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminForceFalukantPregnancy(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { characterId, fatherCharacterId, dueInDays } = req.body;
|
||||||
|
const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, {
|
||||||
|
fatherCharacterId,
|
||||||
|
dueInDays,
|
||||||
|
});
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminGetPotentialFathersForCharacter(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { characterId } = req.params;
|
||||||
|
const response = await AdminService.adminGetPotentialFathersForCharacter(userId, characterId);
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminClearFalukantPregnancy(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { characterId } = req.body;
|
||||||
|
const response = await AdminService.adminClearFalukantPregnancy(userId, characterId);
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminForceFalukantBirth(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body;
|
||||||
|
const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, {
|
||||||
|
fatherCharacterId,
|
||||||
|
birthContext,
|
||||||
|
legitimacy,
|
||||||
|
gender,
|
||||||
|
});
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminCleanupCharacterDeathArtifacts(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const { characterId } = req.params;
|
||||||
|
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess'
|
||||||
|
? 403
|
||||||
|
: (['invalidCharacter', 'notfound', 'targetnotdead'].includes(error.message) ? 400 : 500);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getFalukantUserBranches(req, res) {
|
async getFalukantUserBranches(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
@@ -432,6 +586,42 @@ class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFalukantAllRegions(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const regions = await AdminService.getFalukantAllRegions(userId);
|
||||||
|
res.status(200).json(regions);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFalukantRegionTypes(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const types = await AdminService.getFalukantRegionTypes(userId);
|
||||||
|
res.status(200).json(types);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 500;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFalukantRegion(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const created = await AdminService.createFalukantRegion(userId, req.body || {});
|
||||||
|
res.status(200).json(created);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
const status = error.message === 'noaccess' ? 403 : 400;
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateFalukantRegionMap(req, res) {
|
async updateFalukantRegionMap(req, res) {
|
||||||
try {
|
try {
|
||||||
const { userid: userId } = req.headers;
|
const { userid: userId } = req.headers;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class AuthController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'credentialsinvalid') {
|
if (error.message === 'credentialsinvalid') {
|
||||||
res.status(404).json({ error: error.message });
|
res.status(404).json({ error: error.message });
|
||||||
|
} else if (error.message === 'userblocked') {
|
||||||
|
res.status(403).json({ error: error.message });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ChatController {
|
|||||||
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
|
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
|
||||||
this.getOwnRooms = this.getOwnRooms.bind(this);
|
this.getOwnRooms = this.getOwnRooms.bind(this);
|
||||||
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
|
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
|
||||||
|
this.reportChatIncident = this.reportChatIncident.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMessages(req, res) {
|
async getMessages(req, res) {
|
||||||
@@ -215,6 +216,32 @@ class ChatController {
|
|||||||
res.status(status).json({ error: error.message });
|
res.status(status).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reportChatIncident(req, res) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
context: Joi.string().valid('random_chat', 'multi_chat', 'one_to_one').required(),
|
||||||
|
reporterHashedId: Joi.string().allow('', null),
|
||||||
|
reporterRandomId: Joi.string().allow('', null),
|
||||||
|
reporterUsername: Joi.string().allow('', null),
|
||||||
|
offenderHashedId: Joi.string().allow('', null),
|
||||||
|
offenderRandomId: Joi.string().allow('', null),
|
||||||
|
offenderUsername: Joi.string().allow('', null),
|
||||||
|
incidentAt: Joi.date().iso().required(),
|
||||||
|
chatHistory: Joi.array().min(1).required(),
|
||||||
|
metadata: Joi.object().unknown(true).optional()
|
||||||
|
});
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await chatService.reportChatIncident(value);
|
||||||
|
return res.status(201).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in reportChatIncident:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatController;
|
export default ChatController;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import FalukantService from '../services/falukantService.js';
|
import FalukantService from '../services/falukantService.js';
|
||||||
|
import politicalPowersService from '../services/falukantPoliticalPowersService.js';
|
||||||
|
|
||||||
function extractHashedUserId(req) {
|
function extractHashedUserId(req) {
|
||||||
return req.headers?.userid;
|
return req.headers?.userid;
|
||||||
@@ -118,6 +119,8 @@ class FalukantController {
|
|||||||
});
|
});
|
||||||
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
||||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
||||||
|
this.improveLoverAffection = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.improveLoverAffection(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
||||||
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
||||||
@@ -207,11 +210,29 @@ class FalukantController {
|
|||||||
}, { blockInDebtorsPrison: true });
|
}, { blockInDebtorsPrison: true });
|
||||||
|
|
||||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||||
|
this.getPoliticalOfficeCatalog = this._wrapWithUser((userId) => this.service.getPoliticalOfficeCatalog(userId));
|
||||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
||||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
||||||
|
|
||||||
|
this.getPoliticalMyPowers = this._wrapWithUser((userId) => politicalPowersService.getMyPowers(userId));
|
||||||
|
this.getPoliticalTaxJurisdiction = this._wrapWithUser((userId) => politicalPowersService.getTaxJurisdiction(userId));
|
||||||
|
this.setPoliticalRegionTax = this._wrapWithUser((userId, req) =>
|
||||||
|
politicalPowersService.setRegionTax(userId, parseInt(req.params.regionId, 10), req.body?.percent), { blockInDebtorsPrison: true });
|
||||||
|
this.getPoliticalRegionTaxHistory = this._wrapWithUser((userId, req) =>
|
||||||
|
politicalPowersService.getRegionTaxHistory(userId, parseInt(req.params.regionId, 10), parseInt(req.query.limit || '5', 10)));
|
||||||
|
this.getPoliticalAppointableOffices = this._wrapWithUser((userId) => politicalPowersService.getAppointableOffices(userId));
|
||||||
|
this.createPoliticalAppointment = this._wrapWithUser(
|
||||||
|
(userId, req) =>
|
||||||
|
politicalPowersService.createAppointment(userId, {
|
||||||
|
targetCharacterId: req.body?.targetCharacterId,
|
||||||
|
officeTypeId: req.body?.officeTypeId,
|
||||||
|
regionId: req.body?.regionId
|
||||||
|
}),
|
||||||
|
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||||
|
);
|
||||||
|
|
||||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||||
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
|
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
|
||||||
@@ -227,7 +248,9 @@ class FalukantController {
|
|||||||
if (Number.isNaN(regionId)) {
|
if (Number.isNaN(regionId)) {
|
||||||
throw new Error('regionId is required');
|
throw new Error('regionId is required');
|
||||||
}
|
}
|
||||||
return this.service.getAllProductPricesInRegion(userId, regionId);
|
const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true';
|
||||||
|
const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null;
|
||||||
|
return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId });
|
||||||
});
|
});
|
||||||
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
const productId = parseInt(req.query.productId, 10);
|
const productId = parseInt(req.query.productId, 10);
|
||||||
@@ -242,11 +265,17 @@ class FalukantController {
|
|||||||
const body = req.body || {};
|
const body = req.body || {};
|
||||||
const items = Array.isArray(body.items) ? body.items : [];
|
const items = Array.isArray(body.items) ? body.items : [];
|
||||||
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
|
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
|
||||||
|
const includeTransportCosts = body.includeTransportCosts === true || body.includeTransportCosts === 'true';
|
||||||
const valid = items.map(i => ({
|
const valid = items.map(i => ({
|
||||||
productId: parseInt(i.productId, 10),
|
productId: parseInt(i.productId, 10),
|
||||||
currentPrice: parseFloat(i.currentPrice)
|
currentPrice: parseFloat(i.currentPrice)
|
||||||
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
||||||
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
|
return this.service.getProductPricesInCitiesBatch(
|
||||||
|
userId,
|
||||||
|
valid,
|
||||||
|
Number.isNaN(currentRegionId) ? null : currentRegionId,
|
||||||
|
{ includeTransportCosts }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
|
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
|
||||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
||||||
|
|||||||
79
backend/controllers/moderationController.js
Normal file
79
backend/controllers/moderationController.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import Joi from 'joi';
|
||||||
|
import moderationService from '../services/moderationService.js';
|
||||||
|
|
||||||
|
const moderationController = {
|
||||||
|
async createReport(req, res) {
|
||||||
|
const allowedTargetTypes = [
|
||||||
|
'forum_message',
|
||||||
|
'gallery_image',
|
||||||
|
'guestbook_entry',
|
||||||
|
'one_to_one_message',
|
||||||
|
'diary_entry',
|
||||||
|
'user_profile',
|
||||||
|
'blog',
|
||||||
|
'blog_post'
|
||||||
|
];
|
||||||
|
const schema = Joi.object({
|
||||||
|
targetType: Joi.string().valid(...allowedTargetTypes).required(),
|
||||||
|
targetId: Joi.number().integer().min(1).optional(),
|
||||||
|
targetRef: Joi.string().trim().max(255).allow('').optional(),
|
||||||
|
reason: Joi.string().trim().min(3).max(120).required(),
|
||||||
|
details: Joi.string().allow('').max(2000).optional()
|
||||||
|
}).or('targetId', 'targetRef');
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const result = await moderationService.createReport(userId, value);
|
||||||
|
return res.status(201).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in createReport:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async listReports(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const result = await moderationService.listReports(userId, req.query || {});
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in listReports:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateReportStatus(req, res) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
status: Joi.string().valid('open', 'in_review', 'resolved', 'rejected').required(),
|
||||||
|
reviewerNote: Joi.string().allow('').max(2000).optional()
|
||||||
|
});
|
||||||
|
const { error, value } = schema.validate(req.body || {});
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const result = await moderationService.updateReportStatus(userId, req.params.reportId, value);
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in updateReportStatus:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOpenReportCount(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const result = await moderationService.getOpenReportCount(userId);
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in getOpenReportCount:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default moderationController;
|
||||||
@@ -96,9 +96,7 @@ const menuStructure = {
|
|||||||
},
|
},
|
||||||
eroticChat: {
|
eroticChat: {
|
||||||
visible: ["over18"],
|
visible: ["over18"],
|
||||||
action: "openEroticChat",
|
action: "openEroticChat"
|
||||||
view: "window",
|
|
||||||
class: "eroticChatWindow"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -280,6 +278,10 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "forum"],
|
visible: ["mainadmin", "forum"],
|
||||||
path: "/admin/forum"
|
path: "/admin/forum"
|
||||||
},
|
},
|
||||||
|
moderationReports: {
|
||||||
|
visible: ["mainadmin", "forum"],
|
||||||
|
path: "/admin/moderation/reports"
|
||||||
|
},
|
||||||
chatrooms: {
|
chatrooms: {
|
||||||
visible: ["mainadmin", "chatrooms"],
|
visible: ["mainadmin", "chatrooms"],
|
||||||
path: "/admin/chatrooms"
|
path: "/admin/chatrooms"
|
||||||
@@ -293,7 +295,7 @@ const menuStructure = {
|
|||||||
path: "/admin/interests"
|
path: "/admin/interests"
|
||||||
},
|
},
|
||||||
falukant: {
|
falukant: {
|
||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant", "worker_schedule_read"],
|
||||||
children: {
|
children: {
|
||||||
logentries: {
|
logentries: {
|
||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant"],
|
||||||
@@ -315,6 +317,10 @@ const menuStructure = {
|
|||||||
visible: ["mainadmin", "falukant"],
|
visible: ["mainadmin", "falukant"],
|
||||||
path: "/admin/falukant/create-npc"
|
path: "/admin/falukant/create-npc"
|
||||||
},
|
},
|
||||||
|
workerSchedules: {
|
||||||
|
visible: ["mainadmin", "worker_schedule_read"],
|
||||||
|
path: "/admin/falukant/worker-schedules"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
minigames: {
|
minigames: {
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ class SocialNetworkController {
|
|||||||
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
||||||
this.deleteFolder = this.deleteFolder.bind(this);
|
this.deleteFolder = this.deleteFolder.bind(this);
|
||||||
this.getAdultFolders = this.getAdultFolders.bind(this);
|
this.getAdultFolders = this.getAdultFolders.bind(this);
|
||||||
|
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
|
||||||
this.createAdultFolder = this.createAdultFolder.bind(this);
|
this.createAdultFolder = this.createAdultFolder.bind(this);
|
||||||
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
||||||
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
||||||
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
||||||
this.changeAdultImage = this.changeAdultImage.bind(this);
|
this.changeAdultImage = this.changeAdultImage.bind(this);
|
||||||
this.listEroticVideos = this.listEroticVideos.bind(this);
|
this.listEroticVideos = this.listEroticVideos.bind(this);
|
||||||
|
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
|
||||||
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
||||||
|
this.changeEroticVideo = this.changeEroticVideo.bind(this);
|
||||||
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
||||||
this.reportEroticContent = this.reportEroticContent.bind(this);
|
this.reportEroticContent = this.reportEroticContent.bind(this);
|
||||||
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
||||||
@@ -157,8 +160,8 @@ class SocialNetworkController {
|
|||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
const { imageId } = req.params;
|
const { imageId } = req.params;
|
||||||
const { title, visibilities } = req.body;
|
const { title, visibilities, selectedUsers } = req.body;
|
||||||
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
|
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
|
||||||
console.log('--->', folderId);
|
console.log('--->', folderId);
|
||||||
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -208,6 +211,21 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAdultFoldersByUsername(req, res) {
|
||||||
|
try {
|
||||||
|
const requestingUserId = req.headers.userid;
|
||||||
|
const { username } = req.params;
|
||||||
|
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
|
||||||
|
if (!folders) {
|
||||||
|
return res.status(404).json({ error: 'No folders found or access denied.' });
|
||||||
|
}
|
||||||
|
res.status(200).json(folders);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getAdultFoldersByUsername:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createAdultFolder(req, res) {
|
async createAdultFolder(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
@@ -267,8 +285,8 @@ class SocialNetworkController {
|
|||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
const { imageId } = req.params;
|
const { imageId } = req.params;
|
||||||
const { title, visibilities } = req.body;
|
const { title, visibilities, selectedUsers } = req.body;
|
||||||
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities);
|
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
|
||||||
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in changeAdultImage:', error);
|
console.error('Error in changeAdultImage:', error);
|
||||||
@@ -287,6 +305,18 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEroticVideosByUsername(req, res) {
|
||||||
|
try {
|
||||||
|
const userId = req.headers.userid;
|
||||||
|
const { username } = req.params;
|
||||||
|
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
|
||||||
|
res.status(200).json(videos);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getEroticVideosByUsername:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async uploadEroticVideo(req, res) {
|
async uploadEroticVideo(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
@@ -300,6 +330,18 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeEroticVideo(req, res) {
|
||||||
|
try {
|
||||||
|
const userId = req.headers.userid;
|
||||||
|
const { videoId } = req.params;
|
||||||
|
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
|
||||||
|
res.status(200).json(updatedVideo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in changeEroticVideo:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getEroticVideoByHash(req, res) {
|
async getEroticVideoByHash(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
|
|||||||
@@ -18,15 +18,35 @@ class VocabController {
|
|||||||
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
||||||
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
||||||
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
||||||
|
this.getLanguageDictionary = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.getLanguageDictionary(userId, req.params.languageId, req.query)
|
||||||
|
);
|
||||||
|
|
||||||
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
|
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
|
||||||
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
|
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
|
||||||
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
|
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
|
||||||
|
this.getLessonVocabPool = this._wrapWithUser((userId, req) => this.service.getLessonVocabPool(userId, req.params.lessonId));
|
||||||
|
|
||||||
// Courses
|
// Courses
|
||||||
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
|
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
|
||||||
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
|
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
|
||||||
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
|
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
|
||||||
|
this.getCompletedLessonVocabPool = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.getCompletedLessonVocabPool(userId, req.params.courseId, req.query.untilLessonId)
|
||||||
|
);
|
||||||
|
this.getCourseDictionary = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.getCourseDictionary(userId, req.params.courseId, req.query)
|
||||||
|
);
|
||||||
|
this.getVocabDistractorPool = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.getVocabDistractorPool(userId, req.params.courseId, req.query.beforeLessonId)
|
||||||
|
);
|
||||||
|
this.getCourseSrsDue = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.getCourseSrsDue(userId, req.params.courseId, req.query)
|
||||||
|
);
|
||||||
|
this.reviewSrsItem = this._wrapWithUser((userId, req) =>
|
||||||
|
this.service.reviewSrsItem(userId, req.body),
|
||||||
|
{ successStatus: 201 }
|
||||||
|
);
|
||||||
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
|
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
|
||||||
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
|
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
|
||||||
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
|
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
|
||||||
@@ -41,10 +61,12 @@ class VocabController {
|
|||||||
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
|
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
|
||||||
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
|
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
|
||||||
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
|
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
|
||||||
|
this.getDashboardLearningSummary = this._wrapWithUser((userId) => this.service.getDashboardLearningSummary(userId));
|
||||||
|
|
||||||
// Progress
|
// Progress
|
||||||
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
|
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
|
||||||
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
|
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
|
||||||
|
this.resetLessonProgress = this._wrapWithUser((userId, req) => this.service.resetMyLessonProgress(userId, req.params.lessonId));
|
||||||
|
|
||||||
// Grammar Exercises
|
// Grammar Exercises
|
||||||
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
|
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
|
||||||
@@ -77,4 +99,3 @@ class VocabController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default VocabController;
|
export default VocabController;
|
||||||
|
|
||||||
|
|||||||
94
backend/jobs/politicalBenefitsTick.js
Normal file
94
backend/jobs/politicalBenefitsTick.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Periodischer Job: reputation_periodic für politische Amtsinhaber.
|
||||||
|
* Aufruf: systemd-Timer oder FALUKANT_POLITICAL_REPUTATION_JOB=1 (siehe server.js).
|
||||||
|
*/
|
||||||
|
import PoliticalOffice from '../models/falukant/data/political_office.js';
|
||||||
|
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
|
||||||
|
import PoliticalOfficeBenefit from '../models/falukant/predefine/political_office_benefit.js';
|
||||||
|
import PoliticalOfficeBenefitType from '../models/falukant/type/political_office_benefit_type.js';
|
||||||
|
import PoliticalBenefitLastTick from '../models/falukant/data/political_benefit_last_tick.js';
|
||||||
|
import FalukantCharacter from '../models/falukant/data/character.js';
|
||||||
|
import FalukantUser from '../models/falukant/data/user.js';
|
||||||
|
import User from '../models/community/user.js';
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
import { notifyUser } from '../utils/socket.js';
|
||||||
|
|
||||||
|
export async function runPoliticalReputationTicks() {
|
||||||
|
const offices = await PoliticalOffice.findAll({
|
||||||
|
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['id', 'name'] }]
|
||||||
|
});
|
||||||
|
const toNotify = new Set();
|
||||||
|
let ticks = 0;
|
||||||
|
|
||||||
|
for (const po of offices) {
|
||||||
|
const characterId = po.characterId;
|
||||||
|
const benefitRows = await PoliticalOfficeBenefit.findAll({
|
||||||
|
where: { officeTypeId: po.officeTypeId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: PoliticalOfficeBenefitType,
|
||||||
|
as: 'benefitDefinition',
|
||||||
|
attributes: ['tr'],
|
||||||
|
required: true,
|
||||||
|
where: { tr: 'reputation_periodic' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const br of benefitRows) {
|
||||||
|
const v = br.value && typeof br.value === 'object' ? br.value : {};
|
||||||
|
const intervalDays = Math.max(1, Number(v.intervalDays ?? v.everyDays ?? 7));
|
||||||
|
const gain = Math.max(1, Number(v.gain ?? 1));
|
||||||
|
|
||||||
|
const [tickRow, created] = await PoliticalBenefitLastTick.findOrCreate({
|
||||||
|
where: {
|
||||||
|
characterId,
|
||||||
|
politicalOfficeBenefitId: br.id
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
characterId,
|
||||||
|
politicalOfficeBenefitId: br.id,
|
||||||
|
lastTickAt: new Date(po.createdAt),
|
||||||
|
ticksCount: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseMs = new Date(tickRow.lastTickAt).getTime();
|
||||||
|
const daysSince = Math.floor((Date.now() - baseMs) / 86400000);
|
||||||
|
if (daysSince < intervalDays) continue;
|
||||||
|
|
||||||
|
await sequelize.transaction(async (t) => {
|
||||||
|
const ch = await FalukantCharacter.findByPk(characterId, { transaction: t });
|
||||||
|
if (!ch) return;
|
||||||
|
const nextRep = Math.min(100, (ch.reputation ?? 0) + gain);
|
||||||
|
await ch.update({ reputation: nextRep }, { transaction: t });
|
||||||
|
await PoliticalBenefitLastTick.update(
|
||||||
|
{
|
||||||
|
lastTickAt: new Date(),
|
||||||
|
ticksCount: (tickRow.ticksCount || 0) + 1
|
||||||
|
},
|
||||||
|
{ where: { id: tickRow.id }, transaction: t }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ticks += 1;
|
||||||
|
toNotify.add(characterId);
|
||||||
|
console.info(
|
||||||
|
`[PoliticalBenefits] reputation_tick characterId=${characterId} benefitId=${br.id} gain=${gain}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const characterId of toNotify) {
|
||||||
|
const ch = await FalukantCharacter.findByPk(characterId, { attributes: ['userId'] });
|
||||||
|
if (!ch?.userId) continue;
|
||||||
|
const fu = await FalukantUser.findOne({
|
||||||
|
where: { id: ch.userId },
|
||||||
|
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
|
||||||
|
});
|
||||||
|
const hid = fu?.user?.hashedId;
|
||||||
|
if (hid) notifyUser(hid, 'falukantUpdateStatus', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { processedOffices: offices.length, ticksApplied: ticks };
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ export const authenticate = async (req, res, next) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||||
}
|
}
|
||||||
|
if (!user.active) {
|
||||||
|
return res.status(403).json({ error: 'Unauthorized: User blocked' });
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await updateUserTimestamp(user.id);
|
await updateUserTimestamp(user.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
0
backend/migrations-active/.gitkeep
Normal file
0
backend/migrations-active/.gitkeep
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
const table = { tableName: 'contact_message', schema: 'service' };
|
||||||
|
const columns = await queryInterface.describeTable(table);
|
||||||
|
|
||||||
|
if (!columns.answer) {
|
||||||
|
await queryInterface.addColumn(table, 'answer', {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columns.answered_at) {
|
||||||
|
await queryInterface.addColumn(table, 'answered_at', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columns.is_answered) {
|
||||||
|
await queryInterface.addColumn(table, 'is_answered', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
const table = { tableName: 'contact_message', schema: 'service' };
|
||||||
|
const columns = await queryInterface.describeTable(table);
|
||||||
|
|
||||||
|
if (columns.answer) {
|
||||||
|
await queryInterface.removeColumn(table, 'answer');
|
||||||
|
}
|
||||||
|
if (columns.answered_at) {
|
||||||
|
await queryInterface.removeColumn(table, 'answered_at');
|
||||||
|
}
|
||||||
|
if (columns.is_answered) {
|
||||||
|
await queryInterface.removeColumn(table, 'is_answered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.createTable(
|
||||||
|
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
erotic_video_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'erotic_video' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
visibility_type_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'type', tableName: 'image_visibility' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryInterface.createTable(
|
||||||
|
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
erotic_video_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'erotic_video' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'user' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
|
||||||
|
SELECT ev.id, iv.id
|
||||||
|
FROM community.erotic_video ev
|
||||||
|
CROSS JOIN type.image_visibility iv
|
||||||
|
WHERE iv.description = 'adults'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM community.erotic_video_image_visibility eviv
|
||||||
|
WHERE eviv.erotic_video_id = ev.id
|
||||||
|
AND eviv.visibility_type_id = iv.id
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
|
||||||
|
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data."character"
|
||||||
|
ADD COLUMN pregnancy_father_character_id INTEGER NULL
|
||||||
|
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
ADD COLUMN IF NOT EXISTS didactic_mode TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS phase_label TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS block_number INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS difficulty_weight INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS new_unit_target INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS review_weight INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_intensive_review BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.didactic_mode IS
|
||||||
|
'Didaktischer Modus der Lektion, z.B. core_input, guided_dialogue, intensive_review oder checkpoint.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.phase_label IS
|
||||||
|
'Übergeordnete Lernphase, z.B. quickstart, daily_life oder stabilization.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.block_number IS
|
||||||
|
'Inhaltlicher Block für Konsolidierungs- und Wiederholungswellen.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.difficulty_weight IS
|
||||||
|
'Grobe relative Schwierigkeit der Lektion von leicht bis schwer.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.new_unit_target IS
|
||||||
|
'Empfohlene Zahl neuer Spracheinheiten in dieser Lektion.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.review_weight IS
|
||||||
|
'Wie stark Wiederholung in dieser Lektion dominieren soll, typischerweise 0 bis 100.';
|
||||||
|
COMMENT ON COLUMN community.vocab_course_lesson.is_intensive_review IS
|
||||||
|
'Markiert Lektionen, die als intensive Wiederholungsphase gedacht sind.';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_lesson
|
||||||
|
DROP COLUMN IF EXISTS is_intensive_review,
|
||||||
|
DROP COLUMN IF EXISTS review_weight,
|
||||||
|
DROP COLUMN IF EXISTS new_unit_target,
|
||||||
|
DROP COLUMN IF EXISTS difficulty_weight,
|
||||||
|
DROP COLUMN IF EXISTS block_number,
|
||||||
|
DROP COLUMN IF EXISTS phase_label,
|
||||||
|
DROP COLUMN IF EXISTS didactic_mode;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_progress
|
||||||
|
ADD COLUMN IF NOT EXISTS lesson_state JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN community.vocab_course_progress.lesson_state IS
|
||||||
|
'Persistierter UI- und Lernzustand pro Nutzer und Lektion fuer Resume im Sprachkurs.';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE community.vocab_course_progress
|
||||||
|
DROP COLUMN IF EXISTS lesson_state;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.falukant_user
|
||||||
|
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'political_office_id'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'office_type_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_predefine.political_office_benefit
|
||||||
|
RENAME COLUMN political_office_id TO office_type_id;
|
||||||
|
ELSIF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'political_office_id'
|
||||||
|
) AND EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'office_type_id'
|
||||||
|
) THEN
|
||||||
|
UPDATE falukant_predefine.political_office_benefit
|
||||||
|
SET office_type_id = COALESCE(office_type_id, political_office_id);
|
||||||
|
ALTER TABLE falukant_predefine.political_office_benefit
|
||||||
|
DROP COLUMN political_office_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.falukant_user
|
||||||
|
DROP COLUMN IF EXISTS last_political_daily_salary_on;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'office_type_id'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'falukant_predefine'
|
||||||
|
AND table_name = 'political_office_benefit'
|
||||||
|
AND column_name = 'political_office_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_predefine.political_office_benefit
|
||||||
|
RENAME COLUMN office_type_id TO political_office_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
|
||||||
|
SELECT upt.id, 'fr', COALESCE(
|
||||||
|
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
|
||||||
|
0
|
||||||
|
) + 1
|
||||||
|
FROM type.user_param upt
|
||||||
|
WHERE upt.description = 'language'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM type.user_param_value x
|
||||||
|
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DELETE FROM type.user_param_value v
|
||||||
|
USING type.user_param upt
|
||||||
|
WHERE v.user_param_type_id = upt.id
|
||||||
|
AND upt.description = 'language'
|
||||||
|
AND v.value = 'fr';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.falukant_user
|
||||||
|
ADD COLUMN IF NOT EXISTS certificate_productions_count_since TIMESTAMPTZ;
|
||||||
|
`);
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
COMMENT ON COLUMN falukant_data.falukant_user.certificate_productions_count_since IS
|
||||||
|
'Daemon/UI: Zählt nur falukant_log.production-Zeilen mit COALESCE(production_timestamp, production_date::timestamp) >= diesem Wert; bei Stufenänderung (Aufstieg/Bankrott/Erbfolge) auf NOW() (YpDaemon QUERY_UPDATE_FALUKANT_USER_CERTIFICATE). NULL = alle passenden Log-Zeilen bis zur ersten Stufenänderung nach Migration. Kein Löschen der Logs zum Reset.';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.falukant_user
|
||||||
|
DROP COLUMN IF EXISTS certificate_productions_count_since;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @param {import('sequelize').QueryInterface} queryInterface */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS falukant_data.political_benefit_last_tick (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
character_id integer NOT NULL
|
||||||
|
REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
||||||
|
political_office_benefit_id integer NOT NULL
|
||||||
|
REFERENCES falukant_predefine.political_office_benefit(id) ON DELETE CASCADE,
|
||||||
|
last_tick_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ticks_count integer NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT political_benefit_last_tick_unique UNIQUE (character_id, political_office_benefit_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS political_benefit_last_tick_character_idx
|
||||||
|
ON falukant_data.political_benefit_last_tick (character_id);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS falukant_data.region_tax_history (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
|
||||||
|
old_tax_percent numeric(12,4) NOT NULL,
|
||||||
|
new_tax_percent numeric(12,4) NOT NULL,
|
||||||
|
setter_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
||||||
|
political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS region_tax_history_region_idx
|
||||||
|
ON falukant_data.region_tax_history (region_id, created_at DESC);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS falukant_data.political_appointment (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
appointer_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
||||||
|
target_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
|
||||||
|
office_type_id integer NOT NULL REFERENCES falukant_type.political_office_type(id) ON DELETE CASCADE,
|
||||||
|
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
|
||||||
|
status varchar(32) NOT NULL DEFAULT 'completed',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at timestamptz NULL,
|
||||||
|
completed_political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS political_appointment_appointer_idx
|
||||||
|
ON falukant_data.political_appointment (appointer_character_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS political_appointment_target_idx
|
||||||
|
ON falukant_data.political_appointment (target_character_id);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TABLE IF EXISTS falukant_data.political_appointment;
|
||||||
|
DROP TABLE IF EXISTS falukant_data.region_tax_history;
|
||||||
|
DROP TABLE IF EXISTS falukant_data.political_benefit_last_tick;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.political_office_type
|
||||||
|
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_type.political_office_type AS pot
|
||||||
|
SET hierarchy_level = sub.lvl
|
||||||
|
FROM (VALUES
|
||||||
|
('assessor', 1),
|
||||||
|
('councillor', 1),
|
||||||
|
('council', 2),
|
||||||
|
('beadle', 2),
|
||||||
|
('town-clerk', 2),
|
||||||
|
('mayor', 3),
|
||||||
|
('master-builder', 2),
|
||||||
|
('village-major', 2),
|
||||||
|
('judge', 3),
|
||||||
|
('bailif', 3),
|
||||||
|
('taxman', 2),
|
||||||
|
('sheriff', 3),
|
||||||
|
('consultant', 3),
|
||||||
|
('treasurer', 4),
|
||||||
|
('hangman', 2),
|
||||||
|
('territorial-council', 3),
|
||||||
|
('territorial-council-speaker', 4),
|
||||||
|
('ruler-consultant', 4),
|
||||||
|
('state-administrator', 4),
|
||||||
|
('super-state-administrator', 5),
|
||||||
|
('governor', 5),
|
||||||
|
('ministry-helper', 4),
|
||||||
|
('minister', 5),
|
||||||
|
('chancellor', 6)
|
||||||
|
) AS sub(name, lvl)
|
||||||
|
WHERE pot.name = sub.name;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_type.political_office_type
|
||||||
|
DROP COLUMN IF EXISTS hierarchy_level;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
|
||||||
|
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ADD COLUMN IF NOT EXISTS marriage_satisfaction integer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE falukant_data.relationship_state
|
||||||
|
SET marriage_satisfaction = 55
|
||||||
|
WHERE marriage_satisfaction IS NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ALTER COLUMN marriage_satisfaction SET DEFAULT 55;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ALTER COLUMN marriage_satisfaction SET NOT NULL;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'relationship_state_marriage_satisfaction_check'
|
||||||
|
AND connamespace = 'falukant_data'::regnamespace
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
ADD CONSTRAINT relationship_state_marriage_satisfaction_check
|
||||||
|
CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
ALTER TABLE falukant_data.relationship_state
|
||||||
|
DROP COLUMN IF EXISTS marriage_satisfaction;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS community.vocab_srs_item (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES community."user"(id) ON DELETE CASCADE,
|
||||||
|
course_id INTEGER NOT NULL REFERENCES community.vocab_course(id) ON DELETE CASCADE,
|
||||||
|
lesson_id INTEGER NULL REFERENCES community.vocab_course_lesson(id) ON DELETE SET NULL,
|
||||||
|
item_key VARCHAR(80) NOT NULL,
|
||||||
|
learning TEXT NOT NULL,
|
||||||
|
reference TEXT NOT NULL,
|
||||||
|
direction VARCHAR(8) NOT NULL DEFAULT 'BOTH',
|
||||||
|
stage INTEGER NOT NULL DEFAULT 0,
|
||||||
|
interval_days INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_reviewed_at TIMESTAMP WITH TIME ZONE NULL,
|
||||||
|
next_due_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
correct_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
wrong_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
lapse_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT vocab_srs_item_user_key_unique UNIQUE (user_id, item_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_due
|
||||||
|
ON community.vocab_srs_item (user_id, course_id, next_due_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_lesson
|
||||||
|
ON community.vocab_srs_item (user_id, course_id, lesson_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE community.vocab_srs_item IS
|
||||||
|
'Nutzerbezogener SRS-Fortschritt pro Vokabel/Phrase aus Sprachkursen.';
|
||||||
|
COMMENT ON COLUMN community.vocab_srs_item.item_key IS
|
||||||
|
'Stabiler deterministischer Schlüssel aus Kurs, Lektion und normalisiertem Begriffspaar.';
|
||||||
|
COMMENT ON COLUMN community.vocab_srs_item.stage IS
|
||||||
|
'SRS-Stufe. Höhere Stufen bedeuten längere Wiederholungsintervalle.';
|
||||||
|
COMMENT ON COLUMN community.vocab_srs_item.next_due_at IS
|
||||||
|
'Zeitpunkt, zu dem das Item wieder fällig ist.';
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TABLE IF EXISTS community.vocab_srs_item;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.addColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'answer', {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'answered_at', {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
await queryInterface.addColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'is_answered', {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.removeColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'answer');
|
|
||||||
await queryInterface.removeColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'answered_at');
|
|
||||||
await queryInterface.removeColumn({
|
|
||||||
tableName: 'contact_message',
|
|
||||||
schema: 'service'
|
|
||||||
}, 'is_answered');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
27
backend/migrations/README.md
Normal file
27
backend/migrations/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Backend-Migrationen (Sequelize)
|
||||||
|
|
||||||
|
## Aktive Migrationen
|
||||||
|
|
||||||
|
Neue Schema-Änderungen: nur noch Dateien unter **`migrations-active/`**. Ausführung z. B.:
|
||||||
|
|
||||||
|
`npm run db:migrate` (siehe `backend/package.json`, Pfad `migrations-active`).
|
||||||
|
|
||||||
|
## Archiv
|
||||||
|
|
||||||
|
Ältere, bereits auf den Umgebungen ausgerollte Migrationen liegen unter **`migrations-archive/`** und werden vom Sequelize-CLI **nicht** mehr ausgeführt.
|
||||||
|
|
||||||
|
Vor dem ersten Deploy nach dieser Aufteilung: fehlende Einträge in `"SequelizeMeta"` für die archivierten Dateinamen per SQL ergänzen, siehe **`sql/baseline-sequelize-meta-migrations-archive.sql`** (idempotent).
|
||||||
|
|
||||||
|
## Sonstiges in diesem Ordner
|
||||||
|
|
||||||
|
SQL-Hilfsdateien und ältere Notizen verbleiben hier (werden nicht vom CLI geladen).
|
||||||
|
|
||||||
|
## Falukant: Zertifikat und Produktionszählung
|
||||||
|
|
||||||
|
| Datei (Archiv) | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| `migrations-archive/20260402140000-add-certificate-productions-count-since.cjs` | Spalte `falukant_data.falukant_user.certificate_productions_count_since` (`TIMESTAMPTZ`, nullable) inkl. Kommentar. Setzt die DB-Grundlage dafür, dass Daemon, Backend und UI dieselbe Periode für „abgeschlossene Produktionen“ nutzen (Filter mit `COALESCE(production_timestamp, production_date::timestamp)` ab diesem Zeitpunkt; `NULL` = bisherige Historie). |
|
||||||
|
|
||||||
|
Eine parallele SQL-Migration im Daemon-Repository (z. B. `014_falukant_certificate_productions_count_since.sql`) kann dieselbe Spalte anlegen, wenn das Deployment dort getrennt ist – Schema doppelt anlegen vermeiden.
|
||||||
|
|
||||||
|
Details zur Zähl- und Retention-Logik: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`.
|
||||||
@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
|||||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||||
|
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
|
||||||
|
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
|
||||||
import GuestbookEntry from './community/guestbook.js';
|
import GuestbookEntry from './community/guestbook.js';
|
||||||
import Forum from './forum/forum.js';
|
import Forum from './forum/forum.js';
|
||||||
import Title from './forum/title.js';
|
import Title from './forum/title.js';
|
||||||
@@ -92,6 +94,9 @@ import Candidate from './falukant/data/candidate.js';
|
|||||||
import Vote from './falukant/data/vote.js';
|
import Vote from './falukant/data/vote.js';
|
||||||
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
||||||
import PoliticalOffice from './falukant/data/political_office.js';
|
import PoliticalOffice from './falukant/data/political_office.js';
|
||||||
|
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
|
||||||
|
import RegionTaxHistory from './falukant/data/region_tax_history.js';
|
||||||
|
import PoliticalAppointment from './falukant/data/political_appointment.js';
|
||||||
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
||||||
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
||||||
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
|
||||||
@@ -121,6 +126,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
|
|||||||
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
||||||
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
||||||
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
||||||
|
import VocabSrsItem from './community/vocab_srs_item.js';
|
||||||
import CalendarEvent from './community/calendar_event.js';
|
import CalendarEvent from './community/calendar_event.js';
|
||||||
import Campaign from './match3/campaign.js';
|
import Campaign from './match3/campaign.js';
|
||||||
import Match3Level from './match3/level.js';
|
import Match3Level from './match3/level.js';
|
||||||
@@ -242,6 +248,17 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'imageId'
|
otherKey: 'imageId'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EroticVideo.belongsToMany(ImageVisibilityType, {
|
||||||
|
through: EroticVideoImageVisibility,
|
||||||
|
foreignKey: 'eroticVideoId',
|
||||||
|
otherKey: 'visibilityTypeId'
|
||||||
|
});
|
||||||
|
ImageVisibilityType.belongsToMany(EroticVideo, {
|
||||||
|
through: EroticVideoImageVisibility,
|
||||||
|
foreignKey: 'visibilityTypeId',
|
||||||
|
otherKey: 'eroticVideoId'
|
||||||
|
});
|
||||||
|
|
||||||
Folder.belongsToMany(ImageVisibilityUser, {
|
Folder.belongsToMany(ImageVisibilityUser, {
|
||||||
through: FolderVisibilityUser,
|
through: FolderVisibilityUser,
|
||||||
foreignKey: 'folderId',
|
foreignKey: 'folderId',
|
||||||
@@ -253,6 +270,19 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'folderId'
|
otherKey: 'folderId'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EroticVideo.belongsToMany(User, {
|
||||||
|
through: EroticVideoVisibilityUser,
|
||||||
|
foreignKey: 'eroticVideoId',
|
||||||
|
otherKey: 'userId',
|
||||||
|
as: 'selectedVisibilityUsers'
|
||||||
|
});
|
||||||
|
User.belongsToMany(EroticVideo, {
|
||||||
|
through: EroticVideoVisibilityUser,
|
||||||
|
foreignKey: 'userId',
|
||||||
|
otherKey: 'eroticVideoId',
|
||||||
|
as: 'visibleEroticVideos'
|
||||||
|
});
|
||||||
|
|
||||||
// Guestbook related associations
|
// Guestbook related associations
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
||||||
@@ -353,6 +383,8 @@ export default function setupAssociations() {
|
|||||||
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
|
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
|
||||||
|
|
||||||
|
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
|
||||||
|
|
||||||
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
|
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
|
||||||
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
|
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
|
||||||
|
|
||||||
@@ -764,6 +796,48 @@ export default function setupAssociations() {
|
|||||||
as: 'heldOffice'
|
as: 'heldOffice'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PoliticalBenefitLastTick.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'character'
|
||||||
|
});
|
||||||
|
FalukantCharacter.hasMany(PoliticalBenefitLastTick, {
|
||||||
|
foreignKey: 'characterId',
|
||||||
|
as: 'politicalBenefitTicks'
|
||||||
|
});
|
||||||
|
PoliticalBenefitLastTick.belongsTo(PoliticalOfficeBenefit, {
|
||||||
|
foreignKey: 'politicalOfficeBenefitId',
|
||||||
|
as: 'officeBenefit'
|
||||||
|
});
|
||||||
|
|
||||||
|
RegionTaxHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
RegionData.hasMany(RegionTaxHistory, { foreignKey: 'regionId', as: 'taxHistory' });
|
||||||
|
RegionTaxHistory.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'setterCharacterId',
|
||||||
|
as: 'setterCharacter'
|
||||||
|
});
|
||||||
|
RegionTaxHistory.belongsTo(PoliticalOffice, {
|
||||||
|
foreignKey: 'politicalOfficeId',
|
||||||
|
as: 'sourceOffice'
|
||||||
|
});
|
||||||
|
|
||||||
|
PoliticalAppointment.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'appointerCharacterId',
|
||||||
|
as: 'appointer'
|
||||||
|
});
|
||||||
|
PoliticalAppointment.belongsTo(FalukantCharacter, {
|
||||||
|
foreignKey: 'targetCharacterId',
|
||||||
|
as: 'targetCharacter'
|
||||||
|
});
|
||||||
|
PoliticalAppointment.belongsTo(PoliticalOfficeType, {
|
||||||
|
foreignKey: 'officeTypeId',
|
||||||
|
as: 'officeType'
|
||||||
|
});
|
||||||
|
PoliticalAppointment.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
PoliticalAppointment.belongsTo(PoliticalOffice, {
|
||||||
|
foreignKey: 'completedPoliticalOfficeId',
|
||||||
|
as: 'completedOffice'
|
||||||
|
});
|
||||||
|
|
||||||
// elections
|
// elections
|
||||||
Election.belongsTo(PoliticalOfficeType, {
|
Election.belongsTo(PoliticalOfficeType, {
|
||||||
foreignKey: 'officeTypeId',
|
foreignKey: 'officeTypeId',
|
||||||
@@ -1103,6 +1177,13 @@ export default function setupAssociations() {
|
|||||||
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
|
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
|
||||||
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
|
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
|
||||||
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
|
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
|
||||||
|
|
||||||
|
VocabSrsItem.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
User.hasMany(VocabSrsItem, { foreignKey: 'userId', as: 'vocabSrsItems' });
|
||||||
|
VocabSrsItem.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
|
||||||
|
VocabCourse.hasMany(VocabSrsItem, { foreignKey: 'courseId', as: 'srsItems' });
|
||||||
|
VocabSrsItem.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
|
||||||
|
VocabCourseLesson.hasMany(VocabSrsItem, { foreignKey: 'lessonId', as: 'srsItems' });
|
||||||
|
|
||||||
// Calendar associations
|
// Calendar associations
|
||||||
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||||
|
|||||||
26
backend/models/community/erotic_video_image_visibility.js
Normal file
26
backend/models/community/erotic_video_image_visibility.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
eroticVideoId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
visibilityTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'erotic_video_image_visibility',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
schema: 'community'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EroticVideoImageVisibility;
|
||||||
26
backend/models/community/erotic_video_visibility_user.js
Normal file
26
backend/models/community/erotic_video_visibility_user.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
eroticVideoId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'erotic_video_visibility_user',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
schema: 'community'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EroticVideoVisibilityUser;
|
||||||
@@ -8,6 +8,26 @@ function encodeEncryptedValueToBlob(value) {
|
|||||||
return Buffer.from(encrypted, 'utf8');
|
return Buffer.from(encrypted, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Nur echte Adressen zurückgeben — verhindert Anzeige von Base64-/Key-artigem Müll bei fehlender Entschlüsselung. */
|
||||||
|
function looksLikePlausibleEmail(s) {
|
||||||
|
if (typeof s !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const t = s.trim();
|
||||||
|
if (!t || t.length > 254) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEmailCandidate(s) {
|
||||||
|
if (!s || typeof s !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const t = s.trim();
|
||||||
|
return looksLikePlausibleEmail(t) ? t : null;
|
||||||
|
}
|
||||||
|
|
||||||
function decodeEncryptedBlob(value) {
|
function decodeEncryptedBlob(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
@@ -16,8 +36,9 @@ function decodeEncryptedBlob(value) {
|
|||||||
try {
|
try {
|
||||||
const encryptedUtf8 = value.toString('utf8');
|
const encryptedUtf8 = value.toString('utf8');
|
||||||
const decryptedUtf8 = decrypt(encryptedUtf8);
|
const decryptedUtf8 = decrypt(encryptedUtf8);
|
||||||
if (decryptedUtf8) {
|
const fromUtf8 = normalizeEmailCandidate(decryptedUtf8);
|
||||||
return decryptedUtf8;
|
if (fromUtf8) {
|
||||||
|
return fromUtf8;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
|
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
|
||||||
@@ -26,15 +47,16 @@ function decodeEncryptedBlob(value) {
|
|||||||
try {
|
try {
|
||||||
const encryptedHex = value.toString('hex');
|
const encryptedHex = value.toString('hex');
|
||||||
const decryptedHex = decrypt(encryptedHex);
|
const decryptedHex = decrypt(encryptedHex);
|
||||||
if (decryptedHex) {
|
const fromHex = normalizeEmailCandidate(decryptedHex);
|
||||||
return decryptedHex;
|
if (fromHex) {
|
||||||
|
return fromHex;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Email legacy hex decryption failed:', error.message);
|
console.warn('Email legacy hex decryption failed:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return value.toString('utf8');
|
return normalizeEmailCandidate(value.toString('utf8'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Email could not be read as plain text:', error.message);
|
console.warn('Email could not be read as plain text:', error.message);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -48,6 +48,42 @@ VocabCourseLesson.init({
|
|||||||
defaultValue: 'vocab',
|
defaultValue: 'vocab',
|
||||||
field: 'lesson_type'
|
field: 'lesson_type'
|
||||||
},
|
},
|
||||||
|
didacticMode: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'didactic_mode'
|
||||||
|
},
|
||||||
|
phaseLabel: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'phase_label'
|
||||||
|
},
|
||||||
|
blockNumber: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'block_number'
|
||||||
|
},
|
||||||
|
difficultyWeight: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'difficulty_weight'
|
||||||
|
},
|
||||||
|
newUnitTarget: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'new_unit_target'
|
||||||
|
},
|
||||||
|
reviewWeight: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'review_weight'
|
||||||
|
},
|
||||||
|
isIntensiveReview: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
field: 'is_intensive_review'
|
||||||
|
},
|
||||||
audioUrl: {
|
audioUrl: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ VocabCourseProgress.init({
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0
|
defaultValue: 0
|
||||||
},
|
},
|
||||||
|
lessonState: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: {},
|
||||||
|
field: 'lesson_state'
|
||||||
|
},
|
||||||
lastAccessedAt: {
|
lastAccessedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
94
backend/models/community/vocab_srs_item.js
Normal file
94
backend/models/community/vocab_srs_item.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class VocabSrsItem extends Model {}
|
||||||
|
|
||||||
|
VocabSrsItem.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'user_id'
|
||||||
|
},
|
||||||
|
courseId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'course_id'
|
||||||
|
},
|
||||||
|
lessonId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'lesson_id'
|
||||||
|
},
|
||||||
|
itemKey: {
|
||||||
|
type: DataTypes.STRING(80),
|
||||||
|
allowNull: false,
|
||||||
|
field: 'item_key'
|
||||||
|
},
|
||||||
|
learning: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
type: DataTypes.STRING(8),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'BOTH'
|
||||||
|
},
|
||||||
|
stage: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
intervalDays: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'interval_days'
|
||||||
|
},
|
||||||
|
lastReviewedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'last_reviewed_at'
|
||||||
|
},
|
||||||
|
nextDueAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'next_due_at'
|
||||||
|
},
|
||||||
|
correctCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'correct_count'
|
||||||
|
},
|
||||||
|
wrongCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'wrong_count'
|
||||||
|
},
|
||||||
|
lapseCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'lapse_count'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'VocabSrsItem',
|
||||||
|
tableName: 'vocab_srs_item',
|
||||||
|
schema: 'community',
|
||||||
|
timestamps: true,
|
||||||
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default VocabSrsItem;
|
||||||
@@ -45,6 +45,14 @@ FalukantCharacter.init(
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 100
|
max: 100
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
pregnancyDueAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
pregnancyFatherCharacterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -53,7 +61,12 @@ FalukantCharacter.init(
|
|||||||
tableName: 'character',
|
tableName: 'character',
|
||||||
schema: 'falukant_data',
|
schema: 'falukant_data',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
underscored: true}
|
underscored: true,
|
||||||
|
// Spalten erst nach Migration 20260330000000; ohne Exclude würde SELECT/INSERT auf alten DBs fehlschlagen
|
||||||
|
defaultScope: {
|
||||||
|
attributes: { exclude: ['pregnancyDueAt', 'pregnancyFatherCharacterId'] },
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default FalukantCharacter;
|
export default FalukantCharacter;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ Director.init({
|
|||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true},
|
defaultValue: true},
|
||||||
|
autoAdjustIncome: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false},
|
||||||
lastSalaryPayout: {
|
lastSalaryPayout: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
56
backend/models/falukant/data/political_appointment.js
Normal file
56
backend/models/falukant/data/political_appointment.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class PoliticalAppointment extends Model {}
|
||||||
|
|
||||||
|
PoliticalAppointment.init(
|
||||||
|
{
|
||||||
|
appointerCharacterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'appointer_character_id'
|
||||||
|
},
|
||||||
|
targetCharacterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'target_character_id'
|
||||||
|
},
|
||||||
|
officeTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'office_type_id'
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'region_id'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING(32),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'completed'
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'expires_at'
|
||||||
|
},
|
||||||
|
completedPoliticalOfficeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'completed_political_office_id'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'PoliticalAppointment',
|
||||||
|
tableName: 'political_appointment',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
createdAt: 'created_at',
|
||||||
|
updatedAt: false,
|
||||||
|
underscored: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PoliticalAppointment;
|
||||||
40
backend/models/falukant/data/political_benefit_last_tick.js
Normal file
40
backend/models/falukant/data/political_benefit_last_tick.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class PoliticalBenefitLastTick extends Model {}
|
||||||
|
|
||||||
|
PoliticalBenefitLastTick.init(
|
||||||
|
{
|
||||||
|
characterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'character_id'
|
||||||
|
},
|
||||||
|
politicalOfficeBenefitId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'political_office_benefit_id'
|
||||||
|
},
|
||||||
|
lastTickAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'last_tick_at'
|
||||||
|
},
|
||||||
|
ticksCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'ticks_count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'PoliticalBenefitLastTick',
|
||||||
|
tableName: 'political_benefit_last_tick',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PoliticalBenefitLastTick;
|
||||||
46
backend/models/falukant/data/region_tax_history.js
Normal file
46
backend/models/falukant/data/region_tax_history.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Model, DataTypes } from 'sequelize';
|
||||||
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
|
|
||||||
|
class RegionTaxHistory extends Model {}
|
||||||
|
|
||||||
|
RegionTaxHistory.init(
|
||||||
|
{
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'region_id'
|
||||||
|
},
|
||||||
|
oldTaxPercent: {
|
||||||
|
type: DataTypes.DECIMAL(12, 4),
|
||||||
|
allowNull: false,
|
||||||
|
field: 'old_tax_percent'
|
||||||
|
},
|
||||||
|
newTaxPercent: {
|
||||||
|
type: DataTypes.DECIMAL(12, 4),
|
||||||
|
allowNull: false,
|
||||||
|
field: 'new_tax_percent'
|
||||||
|
},
|
||||||
|
setterCharacterId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'setter_character_id'
|
||||||
|
},
|
||||||
|
politicalOfficeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'political_office_id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'RegionTaxHistory',
|
||||||
|
tableName: 'region_tax_history',
|
||||||
|
schema: 'falukant_data',
|
||||||
|
timestamps: true,
|
||||||
|
createdAt: 'created_at',
|
||||||
|
updatedAt: false,
|
||||||
|
underscored: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RegionTaxHistory;
|
||||||
@@ -88,6 +88,16 @@ RelationshipState.init(
|
|||||||
min: 0,
|
min: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
scandalExtraDailyPct: {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
field: 'scandal_extra_daily_pct',
|
||||||
|
},
|
||||||
monthsUnderfunded: {
|
monthsUnderfunded: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ FalukantUser.init({
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 1},
|
defaultValue: 1},
|
||||||
|
certificateProductionsCountSince: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
mainBranchRegionId: {
|
mainBranchRegionId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
@@ -36,6 +40,11 @@ FalukantUser.init({
|
|||||||
lastNobilityAdvanceAt: {
|
lastNobilityAdvanceAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
lastPoliticalDailySalaryOn: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'last_political_daily_salary_on'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { sequelize } from '../../../utils/sequelize.js';
|
|||||||
class UserHouse extends Model { }
|
class UserHouse extends Model { }
|
||||||
|
|
||||||
UserHouse.init({
|
UserHouse.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
},
|
||||||
roofCondition: {
|
roofCondition: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ DayProduction.init({
|
|||||||
productionDate: {
|
productionDate: {
|
||||||
type: DataTypes.DATEONLY,
|
type: DataTypes.DATEONLY,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: sequelize.literal('CURRENT_DATE')}
|
defaultValue: sequelize.literal('CURRENT_DATE')},
|
||||||
|
completionCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1}
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'DayProduction',
|
modelName: 'DayProduction',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// falukant/predefine/political_office_benefit.js
|
// falukant/predefine/political_office_benefit.js
|
||||||
import { Model, DataTypes } from 'sequelize';
|
import { Model, DataTypes } from 'sequelize';
|
||||||
import { sequelize } from '../../../utils/sequelize.js';
|
import { sequelize } from '../../../utils/sequelize.js';
|
||||||
import PoliticalOfficeBenefitType from '../type/political_office_benefit_type.js';
|
|
||||||
|
|
||||||
class PoliticalOfficeBenefit extends Model {}
|
class PoliticalOfficeBenefit extends Model {}
|
||||||
|
|
||||||
@@ -9,31 +8,29 @@ PoliticalOfficeBenefit.init({
|
|||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true},
|
autoIncrement: true
|
||||||
politicalOfficeId: {
|
},
|
||||||
|
officeTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false},
|
allowNull: false,
|
||||||
|
field: 'office_type_id'
|
||||||
|
},
|
||||||
benefitTypeId: {
|
benefitTypeId: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false},
|
allowNull: false,
|
||||||
|
field: 'benefit_type_id'
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
allowNull: false}}, {
|
allowNull: false
|
||||||
sequelize,
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
modelName: 'PoliticalOfficeBenefit',
|
modelName: 'PoliticalOfficeBenefit',
|
||||||
tableName: 'political_office_benefit',
|
tableName: 'political_office_benefit',
|
||||||
schema: 'falukant_predefine',
|
schema: 'falukant_predefine',
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
underscored: true});
|
underscored: true
|
||||||
|
|
||||||
// Association
|
|
||||||
PoliticalOfficeBenefit.belongsTo(PoliticalOfficeBenefitType, {
|
|
||||||
foreignKey: 'benefit_type_id',
|
|
||||||
as: 'benefitType'
|
|
||||||
});
|
|
||||||
PoliticalOfficeBenefitType.hasMany(PoliticalOfficeBenefit, {
|
|
||||||
foreignKey: 'benefit_type_id',
|
|
||||||
as: 'benefits'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PoliticalOfficeBenefit;
|
export default PoliticalOfficeBenefit;
|
||||||
|
|||||||
@@ -17,10 +17,18 @@ PoliticalOfficeType.init({
|
|||||||
regionType: {
|
regionType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false},
|
allowNull: false},
|
||||||
termLength: {
|
termLength: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 0}}, {
|
defaultValue: 0},
|
||||||
|
/** Stufe für Tageshonorar (base + perRank × level) und Sortierung; 1 = niedrigstes politisches Level */
|
||||||
|
hierarchyLevel: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1,
|
||||||
|
field: 'hierarchy_level'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'PoliticalOfficeType',
|
modelName: 'PoliticalOfficeType',
|
||||||
tableName: 'political_office_type',
|
tableName: 'political_office_type',
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
|||||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||||
|
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
|
||||||
|
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
|
||||||
import GuestbookEntry from './community/guestbook.js';
|
import GuestbookEntry from './community/guestbook.js';
|
||||||
import DiaryHistory from './community/diary_history.js';
|
import DiaryHistory from './community/diary_history.js';
|
||||||
import Diary from './community/diary.js';
|
import Diary from './community/diary.js';
|
||||||
@@ -114,6 +116,9 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
|
|||||||
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
|
||||||
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
|
||||||
import PoliticalOffice from './falukant/data/political_office.js';
|
import PoliticalOffice from './falukant/data/political_office.js';
|
||||||
|
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
|
||||||
|
import RegionTaxHistory from './falukant/data/region_tax_history.js';
|
||||||
|
import PoliticalAppointment from './falukant/data/political_appointment.js';
|
||||||
import Election from './falukant/data/election.js';
|
import Election from './falukant/data/election.js';
|
||||||
import Candidate from './falukant/data/candidate.js';
|
import Candidate from './falukant/data/candidate.js';
|
||||||
import Vote from './falukant/data/vote.js';
|
import Vote from './falukant/data/vote.js';
|
||||||
@@ -151,6 +156,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
|
|||||||
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
||||||
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
||||||
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
||||||
|
import VocabSrsItem from './community/vocab_srs_item.js';
|
||||||
import CalendarEvent from './community/calendar_event.js';
|
import CalendarEvent from './community/calendar_event.js';
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
@@ -179,6 +185,8 @@ const models = {
|
|||||||
FolderImageVisibility,
|
FolderImageVisibility,
|
||||||
ImageImageVisibility,
|
ImageImageVisibility,
|
||||||
FolderVisibilityUser,
|
FolderVisibilityUser,
|
||||||
|
EroticVideoImageVisibility,
|
||||||
|
EroticVideoVisibilityUser,
|
||||||
GuestbookEntry,
|
GuestbookEntry,
|
||||||
DiaryHistory,
|
DiaryHistory,
|
||||||
Diary,
|
Diary,
|
||||||
@@ -258,6 +266,9 @@ const models = {
|
|||||||
PoliticalOfficeBenefitType,
|
PoliticalOfficeBenefitType,
|
||||||
PoliticalOfficeBenefit,
|
PoliticalOfficeBenefit,
|
||||||
PoliticalOffice,
|
PoliticalOffice,
|
||||||
|
PoliticalBenefitLastTick,
|
||||||
|
RegionTaxHistory,
|
||||||
|
PoliticalAppointment,
|
||||||
Election,
|
Election,
|
||||||
Candidate,
|
Candidate,
|
||||||
Vote,
|
Vote,
|
||||||
@@ -308,6 +319,7 @@ const models = {
|
|||||||
VocabGrammarExerciseType,
|
VocabGrammarExerciseType,
|
||||||
VocabGrammarExercise,
|
VocabGrammarExercise,
|
||||||
VocabGrammarExerciseProgress,
|
VocabGrammarExerciseProgress,
|
||||||
|
VocabSrsItem,
|
||||||
|
|
||||||
// Calendar
|
// Calendar
|
||||||
CalendarEvent,
|
CalendarEvent,
|
||||||
|
|||||||
@@ -127,26 +127,29 @@ export async function createTriggers() {
|
|||||||
const updateMoney = `
|
const updateMoney = `
|
||||||
CREATE OR REPLACE FUNCTION falukant_data.update_money(
|
CREATE OR REPLACE FUNCTION falukant_data.update_money(
|
||||||
p_falukant_user_id integer,
|
p_falukant_user_id integer,
|
||||||
p_money_change numeric,
|
p_money_change numeric,
|
||||||
p_activity text,
|
p_activity text,
|
||||||
p_changed_by integer DEFAULT NULL
|
p_changed_by integer DEFAULT NULL::integer
|
||||||
)
|
)
|
||||||
RETURNS void
|
RETURNS void
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $function$
|
AS $function$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_money_before numeric(10,2);
|
v_money_before numeric(14,2);
|
||||||
v_money_after numeric(10,2);
|
v_money_after numeric(14,2);
|
||||||
v_moneyflow_id bigint;
|
v_moneyflow_id bigint;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT money
|
SELECT money
|
||||||
INTO v_money_before
|
INTO v_money_before
|
||||||
FROM falukant_data.falukant_user
|
FROM falukant_data.falukant_user
|
||||||
WHERE id = p_falukant_user_id;
|
WHERE id = p_falukant_user_id;
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
IF NOT FOUND THEN
|
||||||
RAISE EXCEPTION 'FalukantUser mit ID % nicht gefunden', p_falukant_user_id;
|
RAISE EXCEPTION 'FalukantUser mit ID % nicht gefunden', p_falukant_user_id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
v_money_after := v_money_before + p_money_change;
|
v_money_after := v_money_before + p_money_change;
|
||||||
|
|
||||||
INSERT INTO falukant_log.moneyflow (
|
INSERT INTO falukant_log.moneyflow (
|
||||||
falukant_user_id,
|
falukant_user_id,
|
||||||
activity,
|
activity,
|
||||||
@@ -160,22 +163,24 @@ export async function createTriggers() {
|
|||||||
p_falukant_user_id,
|
p_falukant_user_id,
|
||||||
p_activity,
|
p_activity,
|
||||||
v_money_before,
|
v_money_before,
|
||||||
NULL, -- Wird gleich aktualisiert
|
NULL, -- wird gleich aktualisiert
|
||||||
p_money_change,
|
p_money_change,
|
||||||
p_changed_by,
|
p_changed_by,
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
RETURNING id INTO v_moneyflow_id;
|
RETURNING id INTO v_moneyflow_id;
|
||||||
|
|
||||||
UPDATE falukant_data.falukant_user
|
UPDATE falukant_data.falukant_user
|
||||||
SET money = v_money_after
|
SET money = v_money_after
|
||||||
WHERE id = p_falukant_user_id;
|
WHERE id = p_falukant_user_id;
|
||||||
|
|
||||||
UPDATE falukant_log.moneyflow
|
UPDATE falukant_log.moneyflow
|
||||||
SET money_after = (
|
SET money_after = (
|
||||||
SELECT money
|
SELECT money
|
||||||
FROM falukant_data.falukant_user
|
FROM falukant_data.falukant_user
|
||||||
WHERE id = p_falukant_user_id
|
WHERE id = p_falukant_user_id
|
||||||
)
|
)
|
||||||
WHERE id = v_moneyflow_id;
|
WHERE id = v_moneyflow_id;
|
||||||
END;
|
END;
|
||||||
$function$;
|
$function$;
|
||||||
`;
|
`;
|
||||||
|
|||||||
150
backend/package-lock.json
generated
150
backend/package-lock.json
generated
@@ -1122,9 +1122,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.17.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
|
||||||
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
|
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/micromatch": {
|
"node_modules/@types/micromatch": {
|
||||||
@@ -1244,9 +1244,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -1273,9 +1273,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
|
||||||
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
|
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"environment": "^1.0.0"
|
"environment": "^1.0.0"
|
||||||
@@ -1317,15 +1317,6 @@
|
|||||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/astral-regex": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@@ -1358,10 +1349,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/base64id": {
|
"node_modules/base64id": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -1493,12 +1487,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
@@ -2156,9 +2153,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dottie": {
|
"node_modules/dottie": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
|
||||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
|
"integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/draco3dgltf": {
|
"node_modules/draco3dgltf": {
|
||||||
@@ -2188,15 +2185,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/editorconfig": {
|
"node_modules/editorconfig": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
|
||||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
"integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@one-ini/wasm": "0.1.1",
|
"@one-ini/wasm": "0.1.1",
|
||||||
"commander": "^10.0.0",
|
"commander": "^10.0.0",
|
||||||
"minimatch": "9.0.1",
|
"minimatch": "^9.0.1",
|
||||||
"semver": "^7.5.3"
|
"semver": "^7.5.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2808,9 +2805,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-east-asian-width": {
|
"node_modules/get-east-asian-width": {
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
|
||||||
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
"integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -2876,21 +2873,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gltf-validator": {
|
"node_modules/gltf-validator": {
|
||||||
"version": "2.0.0-dev.3.10",
|
"version": "2.0.0-dev.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz",
|
||||||
@@ -3404,9 +3386,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/log-update": {
|
"node_modules/log-update": {
|
||||||
@@ -3668,16 +3650,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.1",
|
"version": "10.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||||
"dev": true,
|
"license": "BlueOak-1.0.0",
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^5.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": "18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -3876,9 +3857,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||||
"integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==",
|
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -4168,9 +4149,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -4232,9 +4213,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/property-graph": {
|
"node_modules/property-graph": {
|
||||||
"version": "4.0.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz",
|
||||||
"integrity": "sha512-I0hojAJfTbSCZy3y6xyK29eayxo14v1bj1VPiDkHjTdz33SV6RdfMz2AHnf4ai62Vng2mN5GkaKahkooBIo9gA==",
|
"integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/proto-list": {
|
"node_modules/proto-list": {
|
||||||
@@ -5009,22 +4990,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser": {
|
"node_modules/socket.io-parser": {
|
||||||
"version": "4.2.4",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.1"
|
"debug": "~4.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser/node_modules/debug": {
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -5298,6 +5279,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/table/node_modules/astral-regex": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/table/node_modules/color-convert": {
|
"node_modules/table/node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@@ -5513,9 +5503,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/underscore": {
|
"node_modules/underscore": {
|
||||||
"version": "1.13.7",
|
"version": "1.13.8",
|
||||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
"dev": "NODE_ENV=development node server.js",
|
"dev": "NODE_ENV=development node server.js",
|
||||||
"start-daemon": "node daemonServer.js",
|
"start-daemon": "node daemonServer.js",
|
||||||
"sync-db": "node sync-database.js",
|
"sync-db": "node sync-database.js",
|
||||||
|
"db:migrate": "sequelize-cli db:migrate --config config/sequelize-cli.cjs --migrations-path migrations-active --env production",
|
||||||
"sync-tables": "node sync-tables-only.js",
|
"sync-tables": "node sync-tables-only.js",
|
||||||
"check-connections": "node check-connections.js",
|
"check-connections": "node check-connections.js",
|
||||||
"cleanup-connections": "node cleanup-connections.js",
|
"cleanup-connections": "node cleanup-connections.js",
|
||||||
"diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs",
|
"diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs",
|
||||||
"diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs",
|
"diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs",
|
||||||
|
"sync:vocab-courses": "node scripts/sync-vocab-course-content.js",
|
||||||
"lockfile:sync": "npm install --package-lock-only",
|
"lockfile:sync": "npm install --package-lock-only",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gltf-transform/cli": "^4.3.0",
|
||||||
"amqplib": "^0.10.9",
|
"amqplib": "^0.10.9",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"connect-redis": "^9.0.0",
|
"connect-redis": "^9.0.0",
|
||||||
@@ -43,10 +46,12 @@
|
|||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0"
|
||||||
"@gltf-transform/cli": "^4.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sequelize-cli": "^6.6.5"
|
"sequelize-cli": "^6.6.5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"minimatch": "10.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { authenticate } from '../middleware/authMiddleware.js';
|
import { authenticate } from '../middleware/authMiddleware.js';
|
||||||
import AdminController from '../controllers/adminController.js';
|
import AdminController from '../controllers/adminController.js';
|
||||||
|
import moderationController from '../controllers/moderationController.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const adminController = new AdminController();
|
const adminController = new AdminController();
|
||||||
@@ -25,6 +26,14 @@ router.put('/users/:id/adult-verification', authenticate, adminController.setAdu
|
|||||||
router.get('/users/erotic-moderation', authenticate, adminController.getEroticModerationReports);
|
router.get('/users/erotic-moderation', authenticate, adminController.getEroticModerationReports);
|
||||||
router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adminController.getEroticModerationPreview);
|
router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adminController.getEroticModerationPreview);
|
||||||
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
|
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
|
||||||
|
router.get('/users/:id/vocab-courses', authenticate, adminController.getUserVocabCourses);
|
||||||
|
router.post('/users/:id/vocab-lesson-progress/reset', authenticate, adminController.resetUserVocabLessonProgress);
|
||||||
|
router.post(
|
||||||
|
'/users/:id/vocab-lesson-progress/mark-complete-through',
|
||||||
|
authenticate,
|
||||||
|
adminController.markUserVocabLessonsCompleteThrough
|
||||||
|
);
|
||||||
|
router.get('/vocab/courses/:courseId', authenticate, adminController.getVocabCourseForAdmin);
|
||||||
router.get('/users/:id', authenticate, adminController.getUser);
|
router.get('/users/:id', authenticate, adminController.getUser);
|
||||||
router.put('/users/:id', authenticate, adminController.updateUser);
|
router.put('/users/:id', authenticate, adminController.updateUser);
|
||||||
|
|
||||||
@@ -43,15 +52,26 @@ router.post('/contacts/answer', authenticate, adminController.answerContact);
|
|||||||
router.post('/falukant/searchuser', authenticate, adminController.searchUser);
|
router.post('/falukant/searchuser', authenticate, adminController.searchUser);
|
||||||
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
|
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
|
||||||
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
|
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
|
||||||
|
router.get('/falukant/character/:characterId/potential-fathers', authenticate, adminController.adminGetPotentialFathersForCharacter);
|
||||||
|
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
|
||||||
|
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
|
||||||
|
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
|
||||||
|
router.post('/falukant/character/:characterId/death-cleanup', authenticate, adminController.adminCleanupCharacterDeathArtifacts);
|
||||||
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
|
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
|
||||||
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
|
||||||
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
|
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
|
||||||
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes);
|
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes);
|
||||||
|
router.get('/falukant/region-types', authenticate, adminController.getFalukantRegionTypes);
|
||||||
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
|
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
|
||||||
|
router.get('/falukant/regions/all', authenticate, adminController.getFalukantAllRegions);
|
||||||
|
router.post('/falukant/regions', authenticate, adminController.createFalukantRegion);
|
||||||
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
|
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
|
||||||
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
||||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||||
|
router.get('/moderation/reports', authenticate, moderationController.listReports);
|
||||||
|
router.get('/moderation/reports/open-count', authenticate, moderationController.getOpenReportCount);
|
||||||
|
router.post('/moderation/reports/:reportId/status', authenticate, moderationController.updateReportStatus);
|
||||||
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||||
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||||
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ router.get('/rooms', authenticate, chatController.getRoomList);
|
|||||||
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
|
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
|
||||||
router.get('/my-rooms', authenticate, chatController.getOwnRooms);
|
router.get('/my-rooms', authenticate, chatController.getOwnRooms);
|
||||||
router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom);
|
router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom);
|
||||||
|
router.post('/report', chatController.reportChatIncident);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ router.post('/production', falukantController.createProduction);
|
|||||||
router.get('/production/:branchId', falukantController.getProduction);
|
router.get('/production/:branchId', falukantController.getProduction);
|
||||||
router.get('/stocktypes', falukantController.getStockTypes);
|
router.get('/stocktypes', falukantController.getStockTypes);
|
||||||
router.get('/stockoverview', falukantController.getStockOverview);
|
router.get('/stockoverview', falukantController.getStockOverview);
|
||||||
router.get('/stock/?:branchId', falukantController.getStock);
|
router.get('/stock', falukantController.getStock);
|
||||||
|
router.get('/stock/:branchId', falukantController.getStock);
|
||||||
router.post('/stock', falukantController.createStock);
|
router.post('/stock', falukantController.createStock);
|
||||||
router.get('/products', falukantController.getProducts);
|
router.get('/products', falukantController.getProducts);
|
||||||
router.get('/inventory/?:branchId', falukantController.getInventory);
|
router.get('/inventory', falukantController.getInventory);
|
||||||
|
router.get('/inventory/:branchId', falukantController.getInventory);
|
||||||
router.post('/sell/all', falukantController.sellAllProducts);
|
router.post('/sell/all', falukantController.sellAllProducts);
|
||||||
router.post('/sell', falukantController.sellProduct);
|
router.post('/sell', falukantController.sellProduct);
|
||||||
router.post('/moneyhistory', falukantController.moneyHistory);
|
router.post('/moneyhistory', falukantController.moneyHistory);
|
||||||
@@ -52,6 +54,7 @@ router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpous
|
|||||||
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
||||||
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
|
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
|
||||||
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
|
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
|
||||||
|
router.post('/family/lover/:relationshipId/improve-affection', falukantController.improveLoverAffection);
|
||||||
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
|
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
|
||||||
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
|
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
|
||||||
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
||||||
@@ -93,6 +96,13 @@ router.post('/nobility', falukantController.advanceNobility);
|
|||||||
router.get('/health', falukantController.getHealth);
|
router.get('/health', falukantController.getHealth);
|
||||||
router.post('/health', falukantController.healthActivity);
|
router.post('/health', falukantController.healthActivity);
|
||||||
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
||||||
|
router.get('/politics/offices', falukantController.getPoliticalOfficeCatalog);
|
||||||
|
router.get('/politics/my-powers', falukantController.getPoliticalMyPowers);
|
||||||
|
router.get('/politics/tax-jurisdiction', falukantController.getPoliticalTaxJurisdiction);
|
||||||
|
router.put('/politics/region/:regionId/tax', falukantController.setPoliticalRegionTax);
|
||||||
|
router.get('/politics/region/:regionId/tax-history', falukantController.getPoliticalRegionTaxHistory);
|
||||||
|
router.get('/politics/appointable-offices', falukantController.getPoliticalAppointableOffices);
|
||||||
|
router.post('/politics/appointments', falukantController.createPoliticalAppointment);
|
||||||
router.get('/politics/open', falukantController.getOpenPolitics);
|
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||||
router.post('/politics/open', falukantController.applyForElections);
|
router.post('/politics/open', falukantController.applyForElections);
|
||||||
router.get('/politics/elections', falukantController.getElections);
|
router.get('/politics/elections', falukantController.getElections);
|
||||||
|
|||||||
10
backend/routers/moderationRouter.js
Normal file
10
backend/routers/moderationRouter.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { authenticate } from '../middleware/authMiddleware.js';
|
||||||
|
import moderationController from '../controllers/moderationController.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.use(authenticate);
|
||||||
|
router.post('/reports', moderationController.createReport);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -17,12 +17,15 @@ router.get('/folder/:folderId', socialNetworkController.getFolderImageList);
|
|||||||
router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
|
router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
|
||||||
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
|
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
|
||||||
router.get('/erotic/folders', socialNetworkController.getAdultFolders);
|
router.get('/erotic/folders', socialNetworkController.getAdultFolders);
|
||||||
|
router.get('/profile/erotic/folders/:username', socialNetworkController.getAdultFoldersByUsername);
|
||||||
|
router.get('/profile/erotic/videos/:username', socialNetworkController.getEroticVideosByUsername);
|
||||||
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
|
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
|
||||||
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
|
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
|
||||||
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
|
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
|
||||||
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
|
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
|
||||||
router.get('/erotic/videos', socialNetworkController.listEroticVideos);
|
router.get('/erotic/videos', socialNetworkController.listEroticVideos);
|
||||||
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
|
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
|
||||||
|
router.put('/erotic/videos/:videoId', socialNetworkController.changeEroticVideo);
|
||||||
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
|
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
|
||||||
router.post('/erotic/report', socialNetworkController.reportEroticContent);
|
router.post('/erotic/report', socialNetworkController.reportEroticContent);
|
||||||
router.get('/images/:imageId', socialNetworkController.getImage);
|
router.get('/images/:imageId', socialNetworkController.getImage);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const vocabController = new VocabController();
|
|||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get('/dashboard-widget', vocabController.getDashboardLearningSummary);
|
||||||
|
|
||||||
router.get('/languages', vocabController.listLanguages);
|
router.get('/languages', vocabController.listLanguages);
|
||||||
router.get('/languages/all', vocabController.listAllLanguages);
|
router.get('/languages/all', vocabController.listAllLanguages);
|
||||||
router.post('/languages', vocabController.createLanguage);
|
router.post('/languages', vocabController.createLanguage);
|
||||||
@@ -18,19 +20,26 @@ router.get('/languages/:languageId/chapters', vocabController.listChapters);
|
|||||||
router.post('/languages/:languageId/chapters', vocabController.createChapter);
|
router.post('/languages/:languageId/chapters', vocabController.createChapter);
|
||||||
router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs);
|
router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs);
|
||||||
router.get('/languages/:languageId/search', vocabController.searchVocabs);
|
router.get('/languages/:languageId/search', vocabController.searchVocabs);
|
||||||
|
router.get('/languages/:languageId/dictionary', vocabController.getLanguageDictionary);
|
||||||
|
|
||||||
router.get('/chapters/:chapterId', vocabController.getChapter);
|
router.get('/chapters/:chapterId', vocabController.getChapter);
|
||||||
router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs);
|
router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs);
|
||||||
router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter);
|
router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter);
|
||||||
|
router.get('/lessons/:lessonId/vocab-pool', vocabController.getLessonVocabPool);
|
||||||
|
|
||||||
// Courses
|
// Courses
|
||||||
router.post('/courses', vocabController.createCourse);
|
router.post('/courses', vocabController.createCourse);
|
||||||
router.get('/courses', vocabController.getCourses);
|
router.get('/courses', vocabController.getCourses);
|
||||||
router.get('/courses/my', vocabController.getMyCourses);
|
router.get('/courses/my', vocabController.getMyCourses);
|
||||||
router.post('/courses/find-by-code', vocabController.getCourseByShareCode);
|
router.post('/courses/find-by-code', vocabController.getCourseByShareCode);
|
||||||
|
router.get('/courses/:courseId/completed-lesson-vocabs', vocabController.getCompletedLessonVocabPool);
|
||||||
|
router.get('/courses/:courseId/dictionary', vocabController.getCourseDictionary);
|
||||||
|
router.get('/courses/:courseId/distractor-pool', vocabController.getVocabDistractorPool);
|
||||||
|
router.get('/courses/:courseId/srs/due', vocabController.getCourseSrsDue);
|
||||||
router.get('/courses/:courseId', vocabController.getCourse);
|
router.get('/courses/:courseId', vocabController.getCourse);
|
||||||
router.put('/courses/:courseId', vocabController.updateCourse);
|
router.put('/courses/:courseId', vocabController.updateCourse);
|
||||||
router.delete('/courses/:courseId', vocabController.deleteCourse);
|
router.delete('/courses/:courseId', vocabController.deleteCourse);
|
||||||
|
router.post('/srs/review', vocabController.reviewSrsItem);
|
||||||
|
|
||||||
// Lessons
|
// Lessons
|
||||||
router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse);
|
router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse);
|
||||||
@@ -45,6 +54,7 @@ router.delete('/courses/:courseId/enroll', vocabController.unenrollFromCourse);
|
|||||||
router.get('/courses/:courseId/progress', vocabController.getCourseProgress);
|
router.get('/courses/:courseId/progress', vocabController.getCourseProgress);
|
||||||
router.get('/lessons/:lessonId', vocabController.getLesson);
|
router.get('/lessons/:lessonId', vocabController.getLesson);
|
||||||
router.put('/lessons/:lessonId/progress', vocabController.updateLessonProgress);
|
router.put('/lessons/:lessonId/progress', vocabController.updateLessonProgress);
|
||||||
|
router.delete('/lessons/:lessonId/progress', vocabController.resetLessonProgress);
|
||||||
|
|
||||||
// Grammar Exercises
|
// Grammar Exercises
|
||||||
router.get('/grammar/exercise-types', vocabController.getExerciseTypes);
|
router.get('/grammar/exercise-types', vocabController.getExerciseTypes);
|
||||||
@@ -58,4 +68,3 @@ router.put('/grammar-exercises/:exerciseId', vocabController.updateGrammarExerci
|
|||||||
router.delete('/grammar-exercises/:exerciseId', vocabController.deleteGrammarExercise);
|
router.delete('/grammar-exercises/:exerciseId', vocabController.deleteGrammarExercise);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { sequelize } from '../utils/sequelize.js';
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||||
|
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
|
||||||
|
|
||||||
const LESSONS_TO_ADD = [
|
const LESSONS_TO_ADD = [
|
||||||
{
|
{
|
||||||
@@ -98,6 +99,8 @@ async function addBisayaWeek1Lessons() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pedagogy = getBisayaLessonPedagogy(lessonData.lessonNumber) || {};
|
||||||
|
|
||||||
await VocabCourseLesson.create({
|
await VocabCourseLesson.create({
|
||||||
courseId: course.id,
|
courseId: course.id,
|
||||||
chapterId: null,
|
chapterId: null,
|
||||||
@@ -113,7 +116,14 @@ async function addBisayaWeek1Lessons() {
|
|||||||
speakingPrompts: lessonData.speakingPrompts || [],
|
speakingPrompts: lessonData.speakingPrompts || [],
|
||||||
targetMinutes: lessonData.targetMinutes,
|
targetMinutes: lessonData.targetMinutes,
|
||||||
targetScorePercent: lessonData.targetScorePercent,
|
targetScorePercent: lessonData.targetScorePercent,
|
||||||
requiresReview: lessonData.requiresReview
|
requiresReview: lessonData.requiresReview,
|
||||||
|
didacticMode: pedagogy.didacticMode || null,
|
||||||
|
phaseLabel: pedagogy.phaseLabel || null,
|
||||||
|
blockNumber: pedagogy.blockNumber ?? null,
|
||||||
|
difficultyWeight: pedagogy.difficultyWeight ?? null,
|
||||||
|
newUnitTarget: pedagogy.newUnitTarget ?? null,
|
||||||
|
reviewWeight: pedagogy.reviewWeight ?? null,
|
||||||
|
isIntensiveReview: Boolean(pedagogy.isIntensiveReview)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`);
|
console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`);
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js'
|
|||||||
import VocabCourseProgress from '../models/community/vocab_course_progress.js';
|
import VocabCourseProgress from '../models/community/vocab_course_progress.js';
|
||||||
import VocabGrammarExerciseProgress from '../models/community/vocab_grammar_exercise_progress.js';
|
import VocabGrammarExerciseProgress from '../models/community/vocab_grammar_exercise_progress.js';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
|
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
|
||||||
|
import {
|
||||||
|
BISAYA_DIDACTICS_24_43,
|
||||||
|
BISAYA_LESSONS_24_43_BY_NUMBER,
|
||||||
|
BISAYA_RELATIONSHIP_ANCHOR_DIDACTICS
|
||||||
|
} from './bisaya-course-plan-24-43.js';
|
||||||
|
|
||||||
const LESSON_DIDACTICS = {
|
const LESSON_DIDACTICS = {
|
||||||
'Begrüßungen & Höflichkeit': {
|
'Begrüßungen & Höflichkeit': {
|
||||||
@@ -21,31 +27,158 @@ const LESSON_DIDACTICS = {
|
|||||||
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
||||||
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
|
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
|
||||||
],
|
],
|
||||||
corePatterns: ['Kumusta ka?', 'Maayong buntag.', 'Maayong adlaw.', 'Amping.', 'Babay.', 'Maayo ko.', 'Salamat.', 'Palihug.'],
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||||
|
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
|
||||||
|
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
|
||||||
|
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
|
||||||
|
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
|
||||||
|
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
|
||||||
|
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
|
||||||
|
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
|
||||||
|
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
|
||||||
|
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
|
||||||
|
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
|
||||||
|
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
|
||||||
|
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
|
||||||
|
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
|
||||||
|
{ target: 'Babay.', gloss: 'Tschüss.' },
|
||||||
|
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
|
||||||
|
{ target: 'Salamat.', gloss: 'Danke.' },
|
||||||
|
{ target: 'Palihug.', gloss: 'Bitte.' }
|
||||||
|
],
|
||||||
grammarFocus: [
|
grammarFocus: [
|
||||||
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
|
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
|
||||||
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' }
|
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },
|
||||||
|
{ title: 'Abend und Schlafen', text: 'Im Familienalltag folgen auf einen Abendgruß oft direkte Schlafens- oder Fürsorgeformeln.', example: 'Maayong gabii. / Katulog og maayo.' },
|
||||||
|
{ title: 'Sanfte Schlafensroutine', text: 'Kurze Fragen nach Müdigkeit und kleine Aufforderungen klingen im Familienalltag natürlicher als lange Sätze.', example: 'Kapoy na ka? Matulog na ta. Inom sa og tubig.' },
|
||||||
|
{ title: 'Familiäre Abendformeln', text: 'Am Abend folgen oft sehr kurze Handlungsformeln wie Licht aus, zudecken oder ein Schlafwunsch.', example: 'Patya ang suga. Tabuni ang imong kaugalingon. Damgo og nindot.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
{ title: 'Mini-Gespräch', prompt: 'Begrüße eine Person, frage nach dem Befinden und reagiere höflich.', cue: 'Kumusta ka? Maayo ko. Salamat.' },
|
{ title: 'Mini-Gespräch', prompt: 'Begrüße eine Person, frage nach dem Befinden und reagiere höflich.', cue: 'Kumusta ka? Maayo ko. Salamat.' },
|
||||||
{ title: 'Verabschiedung', prompt: 'Verabschiede dich kurz und wünsche, dass die andere Person auf sich aufpasst.', cue: 'Babay. Amping.' }
|
{ title: 'Verabschiedung', prompt: 'Verabschiede dich kurz und wünsche, dass die andere Person auf sich aufpasst.', cue: 'Babay. Amping.' },
|
||||||
|
{ title: 'Abend und Schlaf', prompt: 'Wünsche einen guten Abend, eine gute Nacht und dass die Person gut schlafen soll.', cue: 'Maayong gabii. Katulog og maayo.' },
|
||||||
|
{ title: 'Schlafensroutine', prompt: 'Frage, ob die Person müde ist, und leite dann sanft zum Schlafengehen über.', cue: 'Kapoy na ka? Matulog na ta. Inom sa og tubig.' },
|
||||||
|
{ title: 'Vor dem Schlafen', prompt: 'Bitte darum, das Licht auszumachen, sich zuzudecken, und wünsche eine gute Nacht bis morgen.', cue: 'Patya ang suga. Tabuni ang imong kaugalingon. Ugma nasad. Damgo og nindot.' }
|
||||||
],
|
],
|
||||||
practicalTasks: [{ title: 'Alltag', text: 'Sprich die Begrüßung dreimal laut und variiere die Antwort.' }]
|
practicalTasks: [{ title: 'Alltag', text: 'Sprich die Begrüßung dreimal laut und variiere die Antwort.' }]
|
||||||
},
|
},
|
||||||
'Familienwörter': {
|
'Familienwörter': {
|
||||||
learningGoals: [
|
learningGoals: [
|
||||||
'Die wichtigsten Familienbezeichnungen sicher erkennen.',
|
'Die wichtigsten Familienbezeichnungen sicher erkennen.',
|
||||||
'Familienmitglieder mit respektvollen Wörtern ansprechen.',
|
'Familienmitglieder und Großeltern mit respektvollen Wörtern ansprechen.',
|
||||||
'Kurze Sätze über die eigene Familie bilden.'
|
'Kurze Sätze über die eigene Familie bilden.'
|
||||||
],
|
],
|
||||||
corePatterns: ['Si Nanay', 'Si Tatay', 'Kuya nako', 'Ate nako'],
|
corePatterns: [
|
||||||
|
{ target: 'Si Nanay.', gloss: 'Das ist Mama.' },
|
||||||
|
{ target: 'Si Tatay.', gloss: 'Das ist Papa.' },
|
||||||
|
{ target: 'Si Kuya nako.', gloss: 'Das ist mein älterer Bruder.' },
|
||||||
|
{ target: 'Si Ate nako.', gloss: 'Das ist meine ältere Schwester.' },
|
||||||
|
{ target: 'Si Dodong nako.', gloss: 'Das ist mein jüngerer Bruder.' },
|
||||||
|
{ target: 'Si Inday nako.', gloss: 'Das ist meine jüngere Schwester.' },
|
||||||
|
{ target: 'Si Lola nako.', gloss: 'Das ist meine Großmutter.' },
|
||||||
|
{ target: 'Si Lolo nako.', gloss: 'Das ist mein Großvater.' }
|
||||||
|
],
|
||||||
grammarFocus: [
|
grammarFocus: [
|
||||||
{ title: 'Respekt in Familienanreden', text: 'Kuya und Ate werden nicht nur in der Familie, sondern auch respektvoll für ältere Personen benutzt.', example: 'Kuya, palihug.' }
|
{ title: 'Respekt in Familienanreden', text: 'Kuya und Ate richtest du an ältere Geschwister (oder respektvoll an andere). Dodong und Inday nutzt du für jüngere Brüder bzw. Schwestern; „Ading“ ist eine weiche Anrede an jüngere Geschwister.', example: 'Kuya, palihug. / Si Dodong nako.' },
|
||||||
|
{ title: 'si als Personenmarker', text: 'Mit "si" markierst du im einfachen Satz eine konkrete Person.', example: 'Si Nanay. Si Tatay.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
{ title: 'Meine Familie', prompt: 'Stelle zwei Familienmitglieder mit einem kurzen Satz vor.', cue: 'Si Nanay. Si Kuya.' }
|
{ title: 'Meine Familie', prompt: 'Stelle vier Familienmitglieder mit kurzen Sätzen vor.', cue: 'Si Nanay. Si Tatay. Si Kuya nako. Si Dodong nako.' }
|
||||||
],
|
],
|
||||||
practicalTasks: [{ title: 'Familienpraxis', text: 'Nenne laut fünf Familienwörter und bilde danach zwei Mini-Sätze.' }]
|
practicalTasks: [{ title: 'Familienpraxis', text: 'Nenne laut die acht Kern-Familienwörter und bilde danach drei Mini-Sätze über deine Familie.' }]
|
||||||
|
},
|
||||||
|
'Überlebenssätze - Teil 1': {
|
||||||
|
learningGoals: [
|
||||||
|
'Zentrale Notfall- und Verständnisfragen schnell abrufen.',
|
||||||
|
'Höflich um Wiederholung, Hilfe und langsamere Sprache bitten.',
|
||||||
|
'Drei Überlebenssätze hintereinander sicher sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Wala ko kasabot.', gloss: 'Ich verstehe nicht.' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
|
||||||
|
{ target: 'Asa ang CR?', gloss: 'Wo ist die Toilette?' },
|
||||||
|
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' },
|
||||||
|
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
|
||||||
|
{ target: 'Unsay pasabot ani?', gloss: 'Was bedeutet das?' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Bitte-Formeln mit palihug', text: '"Palihug" macht Bitten höflich und taucht in vielen Überlebenssätzen auf.', example: 'Palihug ka mubalik? / Tabangi ko, palihug.' },
|
||||||
|
{ title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Wenn du etwas nicht verstehst', prompt: 'Sage, dass du etwas nicht verstehst, und bitte um Wiederholung.', cue: 'Wala ko kasabot. Palihug ka mubalik?' },
|
||||||
|
{ title: 'Soforthilfe', prompt: 'Bitte um Hilfe und frage dann nach der Toilette oder nach der Bedeutung eines Wortes.', cue: 'Tabangi ko, palihug. Asa ang CR?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Alltagsanker', text: 'Sprich alle sechs Überlebenssätze laut durch und ordne sie drei Alltagssituationen zu.' }]
|
||||||
|
},
|
||||||
|
'Familien-Gespräche': {
|
||||||
|
learningGoals: [
|
||||||
|
'Kurze Familiengespräche sicher verstehen.',
|
||||||
|
'Nach Familienmitgliedern fragen und einfache Antworten geben.',
|
||||||
|
'Ein Mini-Gespräch über Hunger und Zuhause nachsprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta ka, Nanay?', gloss: 'Wie geht es dir, Mama?' },
|
||||||
|
{ target: 'Asa si Tatay?', gloss: 'Wo ist Papa?' },
|
||||||
|
{ target: 'Naa siya sa balay.', gloss: 'Er ist zu Hause.' },
|
||||||
|
{ target: 'Kumusta na ang Kuya?', gloss: 'Wie geht es dem älteren Bruder?' },
|
||||||
|
{ target: 'Kumusta na ang Dodong?', gloss: 'Wie geht es dem jüngeren Bruder?' },
|
||||||
|
{ target: 'Kumusta na ang Inday?', gloss: 'Wie geht es der jüngeren Schwester?' },
|
||||||
|
{ target: 'Gutom na ko, Nanay.', gloss: 'Ich habe Hunger, Mama.' },
|
||||||
|
{ target: 'Hapit na ang pagkaon.', gloss: 'Das Essen ist fast fertig.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'naa für Ort und Vorhandensein', text: '"Naa" hilft dir, über Orte und Vorhandensein zu sprechen.', example: 'Naa siya sa balay.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Familien-Mini-Dialog', prompt: 'Frage nach einem Familienmitglied und reagiere dann mit einer kurzen Antwort.', cue: 'Asa si Tatay? Naa siya sa balay.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Gesprächspraxis', text: 'Spiele einen kurzen Familienaustausch mit Frage, Antwort und Fürsorge nach.' }]
|
||||||
|
},
|
||||||
|
'Gefühle & Zuneigung': {
|
||||||
|
learningGoals: [
|
||||||
|
'Wichtige Gefühle und Zuneigungsformeln sicher unterscheiden.',
|
||||||
|
'Freundliche Nähe und Vermissen sprachlich ausdrücken.',
|
||||||
|
'Zwischen positiven und negativen Gefühlen wechseln.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb.' },
|
||||||
|
{ target: 'Ganahan ko nimo.', gloss: 'Ich mag dich.' },
|
||||||
|
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
|
||||||
|
{ target: 'Nalipay ko.', gloss: 'Ich bin glücklich.' },
|
||||||
|
{ target: 'Nasubo ko.', gloss: 'Ich bin traurig.' },
|
||||||
|
{ target: 'Nalipay ko nga nakita ka.', gloss: 'Ich freue mich, dich zu sehen.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'ko für eigene Gefühle', text: 'Viele Gefühlsaussagen bauen direkt auf dem Muster "Gefühl + ko" auf.', example: 'Nalipay ko. Nasubo ko.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Gefühl ausdrücken', prompt: 'Sage, dass du jemanden magst oder vermisst, und ergänze danach ein einfaches Gefühl.', cue: 'Ganahan ko nimo. Nalipay ko.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Herzsprache', text: 'Lies drei Zuneigungsformeln laut und entscheide danach: liebhaben, mögen oder vermissen?' }]
|
||||||
|
},
|
||||||
|
'Überlebenssätze - Teil 2': {
|
||||||
|
learningGoals: [
|
||||||
|
'Weitere zentrale Alltagsfragen sicher sprechen.',
|
||||||
|
'Höflich Entschuldigung, Nachfrage und Hilfesprache verbinden.',
|
||||||
|
'Im Alltag Preise, Dinge und Sprache klar ansprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Tagpila ni?', gloss: 'Wie viel kostet das?' },
|
||||||
|
{ target: 'Unsa ni?', gloss: 'Was ist das?' },
|
||||||
|
{ target: 'Pasensya.', gloss: 'Entschuldigung.' },
|
||||||
|
{ target: 'Dili ko mag-Bisaya.', gloss: 'Ich spreche kein Bisaya.' },
|
||||||
|
{ target: 'Palihug isulat ni.', gloss: 'Bitte schreib das auf.' },
|
||||||
|
{ target: 'Nawala ko.', gloss: 'Ich habe mich verlaufen.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Kurze Markt- und Orientierungssprache', text: 'Kurze Fragewörter plus ein Nomen reichen oft, um im Alltag voranzukommen.', example: 'Tagpila ni? Unsa ni?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Auf dem Markt', prompt: 'Frage nach Preis und Bedeutung eines Gegenstands und bitte dann darum, etwas aufzuschreiben.', cue: 'Tagpila ni? Unsa ni? Palihug isulat ni.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Unterwegs', text: 'Sprich drei Sätze für Einkauf, Nachfrage und Orientierung laut hintereinander.' }]
|
||||||
},
|
},
|
||||||
'Essen & Fürsorge': {
|
'Essen & Fürsorge': {
|
||||||
learningGoals: [
|
learningGoals: [
|
||||||
@@ -53,29 +186,174 @@ const LESSON_DIDACTICS = {
|
|||||||
'Einladungen zum Essen passend beantworten.',
|
'Einladungen zum Essen passend beantworten.',
|
||||||
'Kurze Essens-Dialoge laut üben.'
|
'Kurze Essens-Dialoge laut üben.'
|
||||||
],
|
],
|
||||||
corePatterns: ['Nikaon na ka?', 'Kaon ta.', 'Gusto ka mokaon?', 'Lami kaayo.'],
|
corePatterns: [
|
||||||
|
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
|
||||||
|
{ target: 'Kaon ta.', gloss: 'Lass uns essen.' },
|
||||||
|
{ target: 'Gusto ka mokaon?', gloss: 'Möchtest du essen?' },
|
||||||
|
{ target: 'Gutom na ko.', gloss: 'Ich habe Hunger.' },
|
||||||
|
{ target: 'Palihug, hatagi ko ug tubig.', gloss: 'Bitte gib mir Wasser.' },
|
||||||
|
{ target: 'Salamat sa pagkaon.', gloss: 'Danke für das Essen.' },
|
||||||
|
{ target: 'Busog na ko.', gloss: 'Ich bin satt.' },
|
||||||
|
{ target: 'Lami kaayo.', gloss: 'Sehr lecker.' }
|
||||||
|
],
|
||||||
grammarFocus: [
|
grammarFocus: [
|
||||||
{ title: 'na als Zustandsmarker', text: '"na" markiert oft etwas, das bereits eingetreten ist oder jetzt gilt.', example: 'Nikaon na ka?' }
|
{ title: 'na als Zustandsmarker', text: '"na" markiert oft etwas, das bereits eingetreten ist oder jetzt gilt.', example: 'Nikaon na ka?' },
|
||||||
|
{ title: 'Bitten mit hatagi ko', text: 'Mit "hatagi ko" bittest du konkret darum, dass dir etwas gegeben wird.', example: 'Palihug, hatagi ko ug tubig.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
{ title: 'Fürsorge-Dialog', prompt: 'Frage, ob jemand schon gegessen hat, und biete Essen oder Wasser an.', cue: 'Nikaon na ka? Gusto ka mokaon?' }
|
{ title: 'Fürsorge-Dialog', prompt: 'Frage, ob jemand schon gegessen hat, und biete Essen oder Wasser an.', cue: 'Nikaon na ka? Gusto ka mokaon?' },
|
||||||
|
{ title: 'Beim Essen reagieren', prompt: 'Sage, dass du Hunger hast, bitte um Wasser und reagiere danach auf das Essen.', cue: 'Gutom na ko. Palihug, hatagi ko ug tubig. Lami kaayo.' }
|
||||||
],
|
],
|
||||||
practicalTasks: [{ title: 'Rollenspiel', text: 'Spiele ein kurzes Gespräch zwischen Gastgeber und Gast beim Essen.' }]
|
practicalTasks: [{ title: 'Rollenspiel', text: 'Spiele ein kurzes Gespräch zwischen Gastgeber und Gast beim Essen.' }]
|
||||||
},
|
},
|
||||||
'Zeitformen - Grundlagen': {
|
'Essen & Trinken': {
|
||||||
learningGoals: [
|
learningGoals: [
|
||||||
'Ni- und Mo- als einfache Zeitmarker unterscheiden.',
|
'Wichtige Essens- und Trinkwörter schnell erkennen.',
|
||||||
'Kurze Sätze in Vergangenheit und Zukunft bilden.',
|
'Zwischen Grundnahrungsmitteln, Getränken und Beilagen unterscheiden.',
|
||||||
'Das Muster laut mit mehreren Verben wiederholen.'
|
'Mit den neuen Wörtern kurze Einkaufs- oder Tischsätze bauen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kan-on', gloss: 'gekochter Reis' },
|
||||||
|
{ target: 'Tubig', gloss: 'Wasser' },
|
||||||
|
{ target: 'Isda', gloss: 'Fisch' },
|
||||||
|
{ target: 'Manok', gloss: 'Huhn' },
|
||||||
|
{ target: 'Gulay', gloss: 'Gemüse' },
|
||||||
|
{ target: 'Prutas', gloss: 'Obst' },
|
||||||
|
{ target: 'Gatas', gloss: 'Milch' }
|
||||||
],
|
],
|
||||||
corePatterns: ['Ni-kaon ko.', 'Mo-kaon ko.', 'Ni-adto ko.', 'Mo-adto ko.'],
|
|
||||||
grammarFocus: [
|
grammarFocus: [
|
||||||
{ title: 'Zeitpräfixe', text: 'Ni- verweist auf Vergangenes, Mo- auf Zukünftiges oder Bevorstehendes.', example: 'Ni-kaon ko. / Mo-kaon ko.' }
|
{ title: 'Wortschatz statt ganzer Sätze', text: 'In dieser Lektion sammelst du bewusst Grundwörter, damit du später kurze Essenssätze daraus bauen kannst.', example: 'Kan-on. Tubig. Isda.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
{ title: 'Vorher und nachher', prompt: 'Sage einen Satz über etwas, das du getan hast, und einen Satz über etwas, das du tun wirst.', cue: 'Ni-kaon ko. Mo-adto ko.' }
|
{ title: 'Auf dem Tisch', prompt: 'Nenne drei Dinge, die auf dem Tisch stehen oder die du essen und trinken möchtest.', cue: 'Kan-on, isda ug tubig.' }
|
||||||
],
|
],
|
||||||
practicalTasks: [{ title: 'Mustertraining', text: 'Nimm ein Verb und sprich es einmal mit Ni- und einmal mit Mo-.' }]
|
practicalTasks: [{ title: 'Küchenrunde', text: 'Zeige nacheinander auf sieben Lebensmittel oder stelle sie dir vor und sprich jedes Wort laut aus.' }]
|
||||||
|
},
|
||||||
|
'Alltagsgespräche - Teil 1': {
|
||||||
|
learningGoals: [
|
||||||
|
'Alltagsaktivitäten in ganzen Sätzen beschreiben.',
|
||||||
|
'Nach Tagesplan, Aufgaben und Rückkehr fragen.',
|
||||||
|
'Kurze Familienabsprachen für den Tag sicher führen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Unsa imong buhat karon?', gloss: 'Was machst du heute?' },
|
||||||
|
{ target: 'Nagluto ko para sa panihapon.', gloss: 'Ich koche für das Abendessen.' },
|
||||||
|
{ target: 'Naglimpyo ko sa balay.', gloss: 'Ich putze das Haus.' },
|
||||||
|
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
||||||
|
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
||||||
|
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
||||||
|
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
|
||||||
|
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Tagesablauf abstimmen', prompt: 'Frage nach dem Plan und sage, was du heute erledigst.', cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Alltagscheck', text: 'Sprich vier Sätze zu deinem heutigen Ablauf: Aufgabe, Erledigung, Rückkehr und Pause.' }]
|
||||||
|
},
|
||||||
|
'Haus & Familie': {
|
||||||
|
learningGoals: [
|
||||||
|
'Wichtige Wörter für Haus, Räume und Familie zuordnen und aussprechen.',
|
||||||
|
'Mit „Naa … sa …“ sagen, wo sich jemand oder etwas im Haus befindet.',
|
||||||
|
'Kurze Sätze über Zuhause und Familie verstehen und nachsprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Balay', gloss: 'Haus' },
|
||||||
|
{ target: 'Kwarto', gloss: 'Zimmer' },
|
||||||
|
{ target: 'Kusina', gloss: 'Küche' },
|
||||||
|
{ target: 'Sala', gloss: 'Wohnzimmer' },
|
||||||
|
{ target: 'Banyo', gloss: 'Badezimmer' },
|
||||||
|
{ target: 'Pultahan', gloss: 'Tür' },
|
||||||
|
{ target: 'Bintana', gloss: 'Fenster' },
|
||||||
|
{ target: 'Atop', gloss: 'Dach' },
|
||||||
|
{ target: 'Pamilya', gloss: 'Familie' },
|
||||||
|
{ target: 'Among pamilya', gloss: 'unsere Familie' },
|
||||||
|
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' },
|
||||||
|
{ target: 'Naa sila sa kusina.', gloss: 'Sie sind in der Küche.' },
|
||||||
|
{ target: 'Asa ang kusina?', gloss: 'Wo ist die Küche?' },
|
||||||
|
{ target: 'Ang among balay.', gloss: 'Unser Haus.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{
|
||||||
|
title: 'Naa … sa … für Ort',
|
||||||
|
text: '„Naa“ drückt aus, dass jemand oder etwas irgendwo ist; „sa“ verbindet mit dem Ort.',
|
||||||
|
example: 'Naa ko sa balay. Naa sila sa kusina.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'among = unser (Plural inklusiv)',
|
||||||
|
text: '„Among“ passt zu „wir/unsere“ im Sinne von Familie oder Gruppe.',
|
||||||
|
example: 'Among pamilya. Ang among balay.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{
|
||||||
|
title: 'Räume benennen',
|
||||||
|
prompt: 'Nenne Küche, Wohnzimmer und Badezimmer auf Bisaya.',
|
||||||
|
cue: 'Kusina, sala, banyo.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Wer ist wo?',
|
||||||
|
prompt: 'Sage, dass du zu Hause bist, und frage, wo die Küche ist.',
|
||||||
|
cue: 'Naa ko sa balay. Asa ang kusina?'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{
|
||||||
|
title: 'Rundgang',
|
||||||
|
text: 'Geh in Gedanken durch dein Zuhause und benenne jeden Raum laut auf Bisaya.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Zeitformen - Grundlagen': {
|
||||||
|
learningGoals: [
|
||||||
|
'Vergangenheit, laufende Handlung und Zukunft in einfachen Alltagssätzen unterscheiden.',
|
||||||
|
'Zeitmarker (ni-, nag-/ga-, mo-) mit Zeitwörtern sinnvoll kombinieren.',
|
||||||
|
'Zwischen denselben Verben in drei Zeitbezügen sicher wechseln.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Ni-kaon ko ganiha.', gloss: 'Ich habe vorhin gegessen.' },
|
||||||
|
{ target: 'Nagkaon ko karon.', gloss: 'Ich esse gerade jetzt.' },
|
||||||
|
{ target: 'Mo-kaon ko unya.', gloss: 'Ich werde später essen.' },
|
||||||
|
{ target: 'Ni-adto ko sa merkado ganiha.', gloss: 'Ich bin vorhin zum Markt gegangen.' },
|
||||||
|
{ target: 'Naa ko sa merkado karon.', gloss: 'Ich bin jetzt auf dem Markt.' },
|
||||||
|
{ target: 'Mo-adto ko sa merkado ugma.', gloss: 'Ich werde morgen zum Markt gehen.' },
|
||||||
|
{ target: 'Nipalit ko og isda ganiha.', gloss: 'Ich habe vorhin Fisch gekauft.' },
|
||||||
|
{ target: 'Mupalit ko og isda ugma.', gloss: 'Ich werde morgen Fisch kaufen.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Vergangenheit mit ni-', text: 'ni- markiert im Grundkurs häufig abgeschlossene Handlungen in der Vergangenheit.', example: 'Ni-kaon ko ganiha.' },
|
||||||
|
{ title: 'Laufende Handlung mit nag-/ga-', text: 'Für gerade laufende oder aktuelle Handlungen wird oft nag-/ga- genutzt, häufig zusammen mit karon.', example: 'Nagkaon ko karon.' },
|
||||||
|
{ title: 'Zukunft/Absicht mit mo-', text: 'mo- markiert im Kurs Zukünftiges oder Vorhaben und wird oft mit unya/ugma kombiniert.', example: 'Mo-adto ko ugma.' },
|
||||||
|
{ title: 'Zeitwörter als Klarsteller', text: 'Wörter wie ganiha, karon und ugma helfen, den Zeitbezug eindeutig zu machen.', example: 'ganiha (vorhin), karon (jetzt), ugma (morgen)' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Dreierschritt Zeit', prompt: 'Formuliere denselben Inhalt nacheinander für Vergangenheit, Gegenwart und Zukunft.', cue: 'Ni-adto ko ganiha. Naa ko diri karon. Mo-adto ko ugma.' },
|
||||||
|
{ title: 'Tagesplanung mit Zeiten', prompt: 'Sage, was du vorhin getan hast, was du jetzt machst und was du später tun wirst.', cue: 'Nipalit ko ganiha. Nagluto ko karon. Mo-kaon ko unya.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Verb-Staffel', text: 'Nimm drei Verben (z. B. kaon, adto, palit) und bilde jeweils Vergangenheit, Gegenwart und Zukunft laut.' },
|
||||||
|
{ title: 'Zeitkarten', text: 'Ziehe zufällig ein Zeitwort (ganiha/karon/unya/ugma) und bilde sofort einen passenden Satz.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Alltagsgespräche - Teil 2': {
|
||||||
|
learningGoals: [
|
||||||
|
'Ziele, Wege und Zeitpunkte im Alltag genauer angeben.',
|
||||||
|
'Verabredungen und Rückkehrzeiten in ganzen Sätzen formulieren.',
|
||||||
|
'Einkaufs- und Familienwege natürlich besprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Asa ka moadto unya?', gloss: 'Wohin gehst du später?' },
|
||||||
|
{ target: 'Moadto ko sa merkado unya.', gloss: 'Ich gehe später zum Markt.' },
|
||||||
|
{ target: 'Unsa imong plano karong gabii?', gloss: 'Was ist dein Plan heute Abend?' },
|
||||||
|
{ target: 'Magkita mi sa silingan karon.', gloss: 'Wir treffen jetzt die Nachbarn.' },
|
||||||
|
{ target: 'Mupalit ko ug pagkaon para sa balay.', gloss: 'Ich kaufe Essen für zu Hause.' },
|
||||||
|
{ target: 'Mahimo ba ta magkita ugma buntag?', gloss: 'Können wir uns morgen früh treffen?' },
|
||||||
|
{ target: 'Unsa orasa ka mouli?', gloss: 'Um wie viel Uhr kommst du nach Hause?' },
|
||||||
|
{ target: 'Mouli ko mga alas sais.', gloss: 'Ich komme gegen sechs nach Hause.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Weg und Zeit planen', prompt: 'Frage nach Ziel und Uhrzeit und gib eine konkrete Antwort.', cue: 'Asa ka moadto unya? Mouli ko mga alas sais.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [{ title: 'Planungsdialog', text: 'Baue einen Mini-Dialog mit Zielort, Uhrzeit und Rückkehr in mindestens vier Sätzen.' }]
|
||||||
},
|
},
|
||||||
'Woche 1 - Wiederholung': {
|
'Woche 1 - Wiederholung': {
|
||||||
learningGoals: [
|
learningGoals: [
|
||||||
@@ -95,9 +373,19 @@ const LESSON_DIDACTICS = {
|
|||||||
'Von einzelnen Wörtern zu kurzen Sätzen übergehen.'
|
'Von einzelnen Wörtern zu kurzen Sätzen übergehen.'
|
||||||
],
|
],
|
||||||
corePatterns: ['Kumusta', 'Salamat', 'Lami', 'Mingaw ko nimo']
|
corePatterns: ['Kumusta', 'Salamat', 'Lami', 'Mingaw ko nimo']
|
||||||
}
|
},
|
||||||
|
...BISAYA_RELATIONSHIP_ANCHOR_DIDACTICS,
|
||||||
|
...BISAYA_DIDACTICS_24_43
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveDidacticsForLesson(lesson) {
|
||||||
|
const direct = LESSON_DIDACTICS[lesson.title];
|
||||||
|
if (direct) return direct;
|
||||||
|
const plannedTitle = BISAYA_LESSONS_24_43_BY_NUMBER[Number(lesson.lessonNumber)]?.title;
|
||||||
|
if (plannedTitle && LESSON_DIDACTICS[plannedTitle]) return LESSON_DIDACTICS[plannedTitle];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function resetBisayaProgress(courseIds) {
|
async function resetBisayaProgress(courseIds) {
|
||||||
if (courseIds.length === 0) return { lessonProgress: 0, exerciseProgress: 0 };
|
if (courseIds.length === 0) return { lessonProgress: 0, exerciseProgress: 0 };
|
||||||
|
|
||||||
@@ -170,15 +458,36 @@ async function applyBisayaCourseRefresh() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const lesson of lessons) {
|
for (const lesson of lessons) {
|
||||||
const didactics = LESSON_DIDACTICS[lesson.title];
|
const didactics = resolveDidacticsForLesson(lesson);
|
||||||
if (!didactics) continue;
|
const plannedLesson = BISAYA_LESSONS_24_43_BY_NUMBER[Number(lesson.lessonNumber)];
|
||||||
|
const pedagogy = getBisayaLessonPedagogy(lesson.lessonNumber);
|
||||||
|
if (!didactics && !pedagogy) continue;
|
||||||
|
const normalizedDidactics = didactics || {};
|
||||||
|
|
||||||
await lesson.update({
|
await lesson.update({
|
||||||
learningGoals: didactics.learningGoals || [],
|
...(plannedLesson ? {
|
||||||
corePatterns: didactics.corePatterns || [],
|
title: plannedLesson.title,
|
||||||
grammarFocus: didactics.grammarFocus || [],
|
description: plannedLesson.desc,
|
||||||
speakingPrompts: didactics.speakingPrompts || [],
|
weekNumber: plannedLesson.week,
|
||||||
practicalTasks: didactics.practicalTasks || []
|
dayNumber: plannedLesson.day,
|
||||||
|
lessonType: plannedLesson.type,
|
||||||
|
culturalNotes: plannedLesson.cultural,
|
||||||
|
targetMinutes: plannedLesson.targetMin,
|
||||||
|
targetScorePercent: plannedLesson.targetScore,
|
||||||
|
requiresReview: plannedLesson.review
|
||||||
|
} : {}),
|
||||||
|
learningGoals: normalizedDidactics.learningGoals || [],
|
||||||
|
corePatterns: normalizedDidactics.corePatterns || [],
|
||||||
|
grammarFocus: normalizedDidactics.grammarFocus || [],
|
||||||
|
speakingPrompts: normalizedDidactics.speakingPrompts || [],
|
||||||
|
practicalTasks: normalizedDidactics.practicalTasks || [],
|
||||||
|
didacticMode: pedagogy?.didacticMode || null,
|
||||||
|
phaseLabel: pedagogy?.phaseLabel || null,
|
||||||
|
blockNumber: pedagogy?.blockNumber ?? null,
|
||||||
|
difficultyWeight: pedagogy?.difficultyWeight ?? null,
|
||||||
|
newUnitTarget: pedagogy?.newUnitTarget ?? null,
|
||||||
|
reviewWeight: pedagogy?.reviewWeight ?? null,
|
||||||
|
isIntensiveReview: Boolean(pedagogy?.isIntensiveReview)
|
||||||
});
|
});
|
||||||
updatedLessons++;
|
updatedLessons++;
|
||||||
}
|
}
|
||||||
|
|||||||
188
backend/scripts/bisaya-course-phase1.js
Normal file
188
backend/scripts/bisaya-course-phase1.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
export const BISAYA_PHASE1_DIDACTICS = {
|
||||||
|
'Begrüßungen & Höflichkeit': {
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||||
|
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
|
||||||
|
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
|
||||||
|
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
|
||||||
|
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
|
||||||
|
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
|
||||||
|
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
|
||||||
|
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
|
||||||
|
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
|
||||||
|
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
|
||||||
|
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
|
||||||
|
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
|
||||||
|
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
|
||||||
|
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
|
||||||
|
{ target: 'Babay.', gloss: 'Tschüss.' },
|
||||||
|
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
|
||||||
|
{ target: 'Salamat.', gloss: 'Danke.' },
|
||||||
|
{ target: 'Palihug.', gloss: 'Bitte.' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volle Didaktik-Snippets für Lektionen, die in update-bisaya-didactics.js gepflegt werden sollen
|
||||||
|
* und als API-Fallback dienen, wenn core_patterns in der DB fehlen (z. B. Kurz-Wiederholung).
|
||||||
|
*/
|
||||||
|
export const BISAYA_DIDACTICS_FRAGMENTS = {
|
||||||
|
'Alltagsgespräche - Teil 1': {
|
||||||
|
learningGoals: [
|
||||||
|
'Alltagsaktivitäten in ganzen Sätzen beschreiben.',
|
||||||
|
'Nach Tagesplan, Aufgaben und Rückkehr fragen.',
|
||||||
|
'Kurze Familienabsprachen für den Tag sicher führen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Unsa imong buhat karon?', gloss: 'Was machst du heute?' },
|
||||||
|
{ target: 'Nagluto ko para sa panihapon.', gloss: 'Ich koche für das Abendessen.' },
|
||||||
|
{ target: 'Naglimpyo ko sa balay.', gloss: 'Ich putze das Haus.' },
|
||||||
|
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
||||||
|
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
||||||
|
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
||||||
|
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
|
||||||
|
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{
|
||||||
|
title: 'Tagesablauf abstimmen',
|
||||||
|
prompt: 'Frage nach dem Plan und sage, was du heute erledigst.',
|
||||||
|
cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{
|
||||||
|
title: 'Alltagscheck',
|
||||||
|
text: 'Sprich vier Sätze zu deinem heutigen Ablauf: Aufgabe, Erledigung, Rückkehr und Pause.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Haus & Familie': {
|
||||||
|
learningGoals: [
|
||||||
|
'Wichtige Wörter für Haus, Räume und Familie zuordnen und aussprechen.',
|
||||||
|
'Mit „Naa … sa …“ sagen, wo sich jemand oder etwas im Haus befindet.',
|
||||||
|
'Kurze Sätze über Zuhause und Familie verstehen und nachsprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Balay', gloss: 'Haus' },
|
||||||
|
{ target: 'Kwarto', gloss: 'Zimmer' },
|
||||||
|
{ target: 'Kusina', gloss: 'Küche' },
|
||||||
|
{ target: 'Sala', gloss: 'Wohnzimmer' },
|
||||||
|
{ target: 'Banyo', gloss: 'Badezimmer' },
|
||||||
|
{ target: 'Pultahan', gloss: 'Tür' },
|
||||||
|
{ target: 'Bintana', gloss: 'Fenster' },
|
||||||
|
{ target: 'Atop', gloss: 'Dach' },
|
||||||
|
{ target: 'Pamilya', gloss: 'Familie' },
|
||||||
|
{ target: 'Among pamilya', gloss: 'unsere Familie' },
|
||||||
|
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' },
|
||||||
|
{ target: 'Naa sila sa kusina.', gloss: 'Sie sind in der Küche.' },
|
||||||
|
{ target: 'Asa ang kusina?', gloss: 'Wo ist die Küche?' },
|
||||||
|
{ target: 'Ang among balay.', gloss: 'Unser Haus.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{
|
||||||
|
title: 'Naa … sa … für Ort',
|
||||||
|
text: '„Naa“ drückt aus, dass jemand oder etwas irgendwo ist; „sa“ verbindet mit dem Ort.',
|
||||||
|
example: 'Naa ko sa balay. Naa sila sa kusina.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'among = unser (Plural inklusiv)',
|
||||||
|
text: '„Among“ passt zu „wir/unsere“ im Sinne von Familie oder Gruppe.',
|
||||||
|
example: 'Among pamilya. Ang among balay.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{
|
||||||
|
title: 'Räume benennen',
|
||||||
|
prompt: 'Nenne Küche, Wohnzimmer und Badezimmer auf Bisaya.',
|
||||||
|
cue: 'Kusina, sala, banyo.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Wer ist wo?',
|
||||||
|
prompt: 'Sage, dass du zu Hause bist, und frage, wo die Küche ist.',
|
||||||
|
cue: 'Naa ko sa balay. Asa ang kusina?'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{
|
||||||
|
title: 'Rundgang',
|
||||||
|
text: 'Geh in Gedanken durch dein Zuhause und benenne jeden Raum laut auf Bisaya.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Alltagsgespräche - Teil 2': {
|
||||||
|
learningGoals: [
|
||||||
|
'Ziele, Wege und Zeitpunkte im Alltag genauer angeben.',
|
||||||
|
'Verabredungen und Rückkehrzeiten in ganzen Sätzen formulieren.',
|
||||||
|
'Einkaufs- und Familienwege natürlich besprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Asa ka moadto unya?', gloss: 'Wohin gehst du später?' },
|
||||||
|
{ target: 'Moadto ko sa merkado unya.', gloss: 'Ich gehe später zum Markt.' },
|
||||||
|
{ target: 'Unsa imong plano karong gabii?', gloss: 'Was ist dein Plan heute Abend?' },
|
||||||
|
{ target: 'Magkita mi sa silingan karon.', gloss: 'Wir treffen jetzt die Nachbarn.' },
|
||||||
|
{ target: 'Mupalit ko ug pagkaon para sa balay.', gloss: 'Ich kaufe Essen für zu Hause.' },
|
||||||
|
{ target: 'Mahimo ba ta magkita ugma buntag?', gloss: 'Können wir uns morgen früh treffen?' },
|
||||||
|
{ target: 'Unsa orasa ka mouli?', gloss: 'Um wie viel Uhr kommst du nach Hause?' },
|
||||||
|
{ target: 'Mouli ko mga alas sais.', gloss: 'Ich komme gegen sechs nach Hause.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{
|
||||||
|
title: 'Weg und Zeit planen',
|
||||||
|
prompt: 'Frage nach Ziel und Uhrzeit und gib eine konkrete Antwort.',
|
||||||
|
cue: 'Asa ka moadto unya? Mouli ko mga alas sais.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{
|
||||||
|
title: 'Planungsdialog',
|
||||||
|
text: 'Baue einen Mini-Dialog mit Zielort, Uhrzeit und Rückkehr in mindestens vier Sätzen.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Ort & Richtung': {
|
||||||
|
learningGoals: [
|
||||||
|
'Fragen nach Ort und Richtung sicher stellen und beantworten.',
|
||||||
|
'„hier“, „dort“ und „unterwegs nach …“ im Alltag unterscheiden.',
|
||||||
|
'Kurze Weg- und Zielangaben mit bekannten Ortswörtern verbinden.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Asa ka?', gloss: 'Wo bist du? / Wohin gehst du?' },
|
||||||
|
{ target: 'Asa ang merkado?', gloss: 'Wo ist der Markt?' },
|
||||||
|
{ target: 'Asa ang simbahan?', gloss: 'Wo ist die Kirche?' },
|
||||||
|
{ target: 'Dinhi ko.', gloss: 'Ich bin hier.' },
|
||||||
|
{ target: 'Naa ko dinhi.', gloss: 'Ich bin hier.' },
|
||||||
|
{ target: 'Didto siya.', gloss: 'Er/sie ist dort.' },
|
||||||
|
{ target: 'Adto ta didto.', gloss: 'Lass uns dorthin gehen.' },
|
||||||
|
{ target: 'Padulong ko sa merkado.', gloss: 'Ich bin auf dem Weg zum Markt.' },
|
||||||
|
{ target: 'Padulong ta didto.', gloss: 'Lass uns dorthin aufbrechen.' },
|
||||||
|
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{
|
||||||
|
title: 'Asa = Wo / Wohin',
|
||||||
|
text: '„Asa“ fragt nach Ort oder Ziel. Mit „ang“ kannst du nach einem bestimmten Ort fragen.',
|
||||||
|
example: 'Asa ka? Asa ang merkado?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'dinhi / didto',
|
||||||
|
text: '„dinhi“ = hier, „didto“ = dort (weg vom Sprecher).',
|
||||||
|
example: 'Naa ko dinhi. Didto ang simbahan.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{
|
||||||
|
title: 'Weg fragen',
|
||||||
|
prompt: 'Frage, wo der Markt ist, und sage dann, dass du dorthin unterwegs bist.',
|
||||||
|
cue: 'Asa ang merkado? Padulong ko sa merkado.'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{
|
||||||
|
title: 'Mini-Route',
|
||||||
|
text: 'Bilde drei Sätze: wo du bist, wohin du willst, und dass du unterwegs bist.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
160
backend/scripts/bisaya-course-phase2-pedagogy.js
Normal file
160
backend/scripts/bisaya-course-phase2-pedagogy.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
export const BISAYA_LESSON_PEDAGOGY = {
|
||||||
|
1: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 2, newUnitTarget: 5, reviewWeight: 15, isIntensiveReview: false },
|
||||||
|
2: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 8, reviewWeight: 20, isIntensiveReview: false },
|
||||||
|
3: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 7, reviewWeight: 20, isIntensiveReview: false },
|
||||||
|
4: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 25, isIntensiveReview: false },
|
||||||
|
5: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 4, reviewWeight: 25, isIntensiveReview: false },
|
||||||
|
6: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 7, reviewWeight: 25, isIntensiveReview: false },
|
||||||
|
7: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 30, isIntensiveReview: false },
|
||||||
|
8: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 6, reviewWeight: 30, isIntensiveReview: false },
|
||||||
|
9: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 90, isIntensiveReview: true },
|
||||||
|
10: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'checkpoint', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: false },
|
||||||
|
11: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 25, isIntensiveReview: false },
|
||||||
|
12: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 7, reviewWeight: 25, isIntensiveReview: false },
|
||||||
|
13: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 30, isIntensiveReview: false },
|
||||||
|
14: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 30, isIntensiveReview: false },
|
||||||
|
15: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
16: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 35, isIntensiveReview: false },
|
||||||
|
17: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 35, isIntensiveReview: false },
|
||||||
|
18: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 7, reviewWeight: 35, isIntensiveReview: false },
|
||||||
|
19: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 8, reviewWeight: 36, isIntensiveReview: false },
|
||||||
|
20: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 7, reviewWeight: 36, isIntensiveReview: false },
|
||||||
|
21: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 6, reviewWeight: 36, isIntensiveReview: false },
|
||||||
|
22: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
|
||||||
|
23: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: false },
|
||||||
|
24: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 35, isIntensiveReview: false },
|
||||||
|
25: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 7, reviewWeight: 35, isIntensiveReview: false },
|
||||||
|
26: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
27: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
28: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
29: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
30: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
31: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
32: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
|
||||||
|
33: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
34: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
35: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
36: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 60, isIntensiveReview: false },
|
||||||
|
37: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
38: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
39: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
|
||||||
|
40: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
|
||||||
|
41: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
42: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
|
||||||
|
43: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
44: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
45: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
|
||||||
|
46: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
47: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
48: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
49: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 90, isIntensiveReview: true },
|
||||||
|
50: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
51: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
52: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
53: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
54: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
55: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
56: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
57: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
58: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
59: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
|
||||||
|
60: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
|
||||||
|
61: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
62: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
|
||||||
|
63: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 60, isIntensiveReview: false },
|
||||||
|
64: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
65: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 45, isIntensiveReview: false },
|
||||||
|
66: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
67: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
68: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
69: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
|
||||||
|
70: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
71: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
72: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
73: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
74: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
75: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
76: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
77: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
78: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
79: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
|
||||||
|
80: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
81: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
82: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
83: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
84: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
85: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
86: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
87: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
88: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
89: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
|
||||||
|
90: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
91: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
92: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
93: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
94: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
95: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
|
||||||
|
96: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
97: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
98: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
99: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
|
||||||
|
100: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
101: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
|
||||||
|
102: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
103: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
|
||||||
|
104: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 60, isIntensiveReview: false },
|
||||||
|
105: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 60, isIntensiveReview: false },
|
||||||
|
106: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
107: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
108: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: true },
|
||||||
|
109: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
|
||||||
|
110: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
111: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
112: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
|
||||||
|
113: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
114: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
|
||||||
|
115: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
116: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 72, isIntensiveReview: false },
|
||||||
|
117: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 80, isIntensiveReview: false },
|
||||||
|
118: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
|
||||||
|
119: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
|
||||||
|
120: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 75, isIntensiveReview: false },
|
||||||
|
121: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
122: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
|
||||||
|
123: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 65, isIntensiveReview: false },
|
||||||
|
124: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 72, isIntensiveReview: false },
|
||||||
|
125: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
|
||||||
|
126: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 82, isIntensiveReview: false },
|
||||||
|
127: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 74, isIntensiveReview: false },
|
||||||
|
128: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
|
||||||
|
129: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
130: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 75, isIntensiveReview: false },
|
||||||
|
131: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
132: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
|
||||||
|
133: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
134: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 74, isIntensiveReview: false },
|
||||||
|
135: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
|
||||||
|
136: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 84, isIntensiveReview: false },
|
||||||
|
137: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 76, isIntensiveReview: false },
|
||||||
|
138: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
|
||||||
|
139: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
140: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 78, isIntensiveReview: false },
|
||||||
|
141: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
|
||||||
|
142: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
|
||||||
|
143: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
|
||||||
|
144: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
|
||||||
|
145: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
|
||||||
|
146: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 1, reviewWeight: 88, isIntensiveReview: false },
|
||||||
|
147: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 80, isIntensiveReview: false },
|
||||||
|
148: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: true },
|
||||||
|
149: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: true },
|
||||||
|
150: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 82, isIntensiveReview: false },
|
||||||
|
151: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
|
||||||
|
152: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
|
||||||
|
153: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 1, reviewWeight: 70, isIntensiveReview: false },
|
||||||
|
154: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 10, reviewWeight: 76, isIntensiveReview: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getBisayaLessonPedagogy(lessonNumber) {
|
||||||
|
return BISAYA_LESSON_PEDAGOGY[Number(lessonNumber)] || null;
|
||||||
|
}
|
||||||
558
backend/scripts/bisaya-course-phase3-extension.js
Normal file
558
backend/scripts/bisaya-course-phase3-extension.js
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
export const BISAYA_PHASE3_DIDACTICS = {
|
||||||
|
'Besuch & Gastfreundschaft': {
|
||||||
|
learningGoals: [
|
||||||
|
'Besuchssituationen sicher eröffnen und freundlich strukturieren.',
|
||||||
|
'Hereinbitten, Sitzplatz anbieten, Dank ausdrücken und einfache Rückfragen stellen.',
|
||||||
|
'Bekannte Familien- und Fürsorgemuster in Besuchsgespräche übertragen.',
|
||||||
|
'Eine kurze Besuchsszene mit mindestens vier eigenen Sätzen sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
|
||||||
|
{ target: 'Lingkod sa.', gloss: 'Setz dich erst einmal.' },
|
||||||
|
{ target: 'Maayong pag-abot.', gloss: 'Willkommen.' },
|
||||||
|
{ target: 'Salamat sa pag-anhi.', gloss: 'Danke fürs Kommen.' },
|
||||||
|
{ target: 'Gimingaw mi nimo.', gloss: 'Wir haben dich vermisst.' },
|
||||||
|
{ target: 'Kumusta ang biyahe?', gloss: 'Wie war die Reise?' },
|
||||||
|
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' },
|
||||||
|
{ target: 'Naa ra mi diri.', gloss: 'Wir sind hier.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'lang und sa als weiche Partikel', text: 'lang und sa machen Aufforderungen oft weniger hart und alltagstauglicher.', example: 'Sulod lang. Lingkod sa.' },
|
||||||
|
{ title: 'mi statt ko', text: 'mi bedeutet wir und passt, wenn du für deine Familie oder Gruppe sprichst.', example: 'Gimingaw mi nimo.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Gast empfangen', prompt: 'Empfange einen Gast, bitte ihn herein, biete einen Sitzplatz an und frage nach der Reise.', cue: 'Maayong pag-abot. Sulod lang. Lingkod sa. Kumusta ang biyahe?' },
|
||||||
|
{ title: 'Familiäre Wärme', prompt: 'Sage, dass ihr die Person vermisst habt und biete Wasser an.', cue: 'Gimingaw mi nimo. Ganahan ka og tubig?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Besuchsszene', text: 'Spiele eine Besuchsszene mit Begrüßung, Einladung, Angebot und Rückfrage.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Besuch & Haushalt': {
|
||||||
|
learningGoals: [
|
||||||
|
'Wichtige Haus- und Besuchswörter sicher erkennen.',
|
||||||
|
'Dinge im Haus zeigen, benennen und in kurzen Sätzen verwenden.',
|
||||||
|
'Besuchswortschatz mit bereits bekannten Fragen verbinden.',
|
||||||
|
'Mindestens sechs Haushaltswörter aktiv abrufen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'bisita', gloss: 'Besuch / Gast' },
|
||||||
|
{ target: 'balay', gloss: 'Haus / Zuhause' },
|
||||||
|
{ target: 'kwarto', gloss: 'Zimmer' },
|
||||||
|
{ target: 'kusina', gloss: 'Küche' },
|
||||||
|
{ target: 'lamisa', gloss: 'Tisch' },
|
||||||
|
{ target: 'lingkuranan', gloss: 'Stuhl' },
|
||||||
|
{ target: 'pultahan', gloss: 'Tür' },
|
||||||
|
{ target: 'Naa sa kusina.', gloss: 'Es ist in der Küche.' },
|
||||||
|
{ target: 'Asa ang lamisa?', gloss: 'Wo ist der Tisch?' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Naa sa ...', text: 'Mit naa sa sagst du, wo etwas ist.', example: 'Naa sa kusina. Naa sa kwarto.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Im Haus zeigen', prompt: 'Sage, wo Tisch, Stuhl und Küche sind.', cue: 'Naa sa kusina. Naa ang lingkuranan diri.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Hausrunde', text: 'Gehe gedanklich durch ein Haus und nenne sechs Dinge mit Ort.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Fragen im Alltag vertiefen': {
|
||||||
|
learningGoals: [
|
||||||
|
'Mehrstufige Frageketten bilden, ohne in lange deutsche Satzstrukturen zu fallen.',
|
||||||
|
'Ziel, Vorhaben, Zeit und Bedeutung höflich erfragen.',
|
||||||
|
'Bei Unsicherheit nachhaken und eine Frage reparieren.',
|
||||||
|
'Drei kurze Folgefragen flüssig hintereinander sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Asa ka padulong?', gloss: 'Wohin gehst du?' },
|
||||||
|
{ target: 'Unsa imong buhaton?', gloss: 'Was wirst du machen?' },
|
||||||
|
{ target: 'Kanus-a ka moadto?', gloss: 'Wann gehst du hin?' },
|
||||||
|
{ target: 'Kinsa imong kuyog?', gloss: 'Mit wem bist du unterwegs?' },
|
||||||
|
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
|
||||||
|
{ target: 'Unsay pasabot ani?', gloss: 'Was bedeutet das?' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
|
||||||
|
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Frageketten', text: 'Bisaya-Alltagsgespräche nutzen oft mehrere kurze Fragen statt einer langen Konstruktion.', example: 'Asa ka padulong? Unsa imong buhaton didto?' },
|
||||||
|
{ title: 'ka und imong', text: 'ka bezieht sich auf du als Person, imong auf dein/deine.', example: 'Asa ka padulong? Unsa imong buhaton?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Drei-Fragen-Kette', prompt: 'Frage nach Ziel, Zeit und Begleitung.', cue: 'Asa ka padulong? Kanus-a ka moadto? Kinsa imong kuyog?' },
|
||||||
|
{ title: 'Reparaturfrage', prompt: 'Sage, dass du fragen möchtest, bitte um Wiederholung und frage nach Bedeutung.', cue: 'Pwede ko mangutana? Palihug ka mubalik? Unsay pasabot ani?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Frageleiter', text: 'Baue aus drei kurzen Fragen eine natürliche Alltagsszene.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Termine & Verabredungen': {
|
||||||
|
learningGoals: [
|
||||||
|
'Einfache Treffen verabreden und bestätigen.',
|
||||||
|
'Zeitangaben mit morgen, später, heute und Uhrzeit verbinden.',
|
||||||
|
'Zusage, Absage und Verschieben weich ausdrücken.',
|
||||||
|
'Einen kurzen Termin-Dialog mit Uhrzeit sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
|
||||||
|
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
|
||||||
|
{ target: 'Pwede ko karon.', gloss: 'Ich kann jetzt / heute.' },
|
||||||
|
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann jetzt / heute nicht.' },
|
||||||
|
{ target: 'Pwede ugma?', gloss: 'Geht morgen?' },
|
||||||
|
{ target: 'Sige, kita ta.', gloss: 'Okay, wir sehen uns.' },
|
||||||
|
{ target: 'Human sa trabaho.', gloss: 'Nach der Arbeit.' },
|
||||||
|
{ target: 'Texti lang ko.', gloss: 'Schreib mir einfach.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'ta für wir beide / lass uns', text: 'ta verbindet dich und die andere Person in gemeinsamen Handlungen.', example: 'Magkita ta ugma. Kita ta.' },
|
||||||
|
{ title: 'pwede für Möglichkeit', text: 'pwede hilft bei Zusage, Frage und Absage.', example: 'Pwede ko karon. Pwede ugma?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Treffen planen', prompt: 'Vereinbare ein Treffen für morgen, frage nach der Uhrzeit und bestätige.', cue: 'Magkita ta ugma. Unsa orasa? Sige, kita ta.' },
|
||||||
|
{ title: 'Termin verschieben', prompt: 'Sage, dass du heute nicht kannst, und schlage morgen vor.', cue: 'Dili ko pwede karon. Pwede ugma?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Kalenderdialog', text: 'Sprich zwei Varianten: eine Zusage und eine weiche Verschiebung.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Woche 5 - Intensivwiederholung I': {
|
||||||
|
learningGoals: [
|
||||||
|
'Besuch, Familie, Fürsorge und Terminplanung unter Abrufdruck mischen.',
|
||||||
|
'Neue Muster mit alten Grundlagen aus Begrüßung, Essen, Zeit und Beziehung verbinden.',
|
||||||
|
'Schnell zwischen Rollen wechseln: Gastgeber, Gast, Familienmitglied.',
|
||||||
|
'Mindestens acht Situationsantworten ohne deutsche Stütze geben.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||||
|
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
|
||||||
|
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
|
||||||
|
{ target: 'Lingkod sa.', gloss: 'Setz dich erst einmal.' },
|
||||||
|
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
|
||||||
|
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
|
||||||
|
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' },
|
||||||
|
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb / Ich liebe dich.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Rollenwechsel', prompt: 'Wechsle zwischen Gast empfangen, nach Essen fragen und Termin vereinbaren.', cue: 'Sulod lang. Nikaon na ka? Magkita ta ugma.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Acht schnelle Antworten', text: 'Beantworte acht kurze Situationen aus Besuch, Familie und Terminplanung laut.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Spiralwiederholung - Familie & Fürsorge': {
|
||||||
|
learningGoals: [
|
||||||
|
'Frühe Familien- und Fürsorgemuster aktiv festigen.',
|
||||||
|
'Familienwörter nicht isoliert, sondern in kurzen Handlungen verwenden.',
|
||||||
|
'Nähe, Essen, Müdigkeit und Hilfe in einem Mini-Dialog verbinden.',
|
||||||
|
'Alte Kernmuster mit neuen Besuchssätzen kombinieren.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Nanay', gloss: 'Mutter' },
|
||||||
|
{ target: 'Tatay', gloss: 'Vater' },
|
||||||
|
{ target: 'anak', gloss: 'Kind' },
|
||||||
|
{ target: 'pamilya', gloss: 'Familie' },
|
||||||
|
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
|
||||||
|
{ target: 'Kapoy na ka?', gloss: 'Bist du schon müde?' },
|
||||||
|
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
|
||||||
|
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb / Ich liebe dich.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Familien-Mini-Dialog', prompt: 'Sprich mit einem Familienmitglied über Essen, Müdigkeit und Hilfe.', cue: 'Nikaon na ka? Kapoy na ka? Tabangi ko, palihug.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Spiralabruf', text: 'Nimm vier alte Familienwörter und bilde mit jedem einen kurzen Alltagssatz.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Gesundheit im Alltag': {
|
||||||
|
learningGoals: [
|
||||||
|
'Einfache Beschwerden erfragen, beantworten und fürsorglich reagieren.',
|
||||||
|
'Körper, Medizin, Ruhe und Wasser in einem Pflegegespräch verbinden.',
|
||||||
|
'Zwischen Frage, Rat und Rückmeldung unterscheiden.',
|
||||||
|
'Ein Gesundheitsgespräch mit mindestens fünf Sätzen sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Sakit imong ulo?', gloss: 'Tut dein Kopf weh?' },
|
||||||
|
{ target: 'Sakit akong tiyan.', gloss: 'Mein Bauch tut weh.' },
|
||||||
|
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
|
||||||
|
{ target: 'Magpahuway sa.', gloss: 'Ruh dich erst einmal aus.' },
|
||||||
|
{ target: 'Uminom og tubig.', gloss: 'Trink Wasser.' },
|
||||||
|
{ target: 'Mas maayo na ka?', gloss: 'Geht es dir schon besser?' },
|
||||||
|
{ target: 'Maayo ra ko.', gloss: 'Mir geht es okay.' },
|
||||||
|
{ target: 'Tawag ta og doktor?', gloss: 'Sollen wir einen Arzt rufen?' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'akong und imong', text: 'akong bedeutet mein, imong bedeutet dein. Beides steht direkt vor dem Körperteil.', example: 'akong tiyan, imong ulo' },
|
||||||
|
{ title: 'Frage mit na', text: 'na kann schon/jetzt markieren und kommt in Gesundheitsfragen häufig vor.', example: 'Mas maayo na ka?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Pflegegespräch', prompt: 'Frage nach Schmerzen, biete Ruhe und Wasser an und frage nach Besserung.', cue: 'Sakit imong ulo? Magpahuway sa. Uminom og tubig. Mas maayo na ka?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Fünf-Satz-Szene', text: 'Sprich eine Szene: Beschwerde, Nachfrage, Medizin, Ruhe, Besserung.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Medikamente & Beschwerden': {
|
||||||
|
learningGoals: [
|
||||||
|
'Gesundheitswortschatz zu Symptomen, Körper und Hilfe sicher abrufen.',
|
||||||
|
'Symptomwörter in kurze Fürsorgesätze einbauen.',
|
||||||
|
'Zwischen Wortabruf und situativer Reaktion wechseln.',
|
||||||
|
'Mindestens acht Gesundheitsbegriffe aktiv verwenden.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'tambal', gloss: 'Medizin' },
|
||||||
|
{ target: 'ubo', gloss: 'Husten' },
|
||||||
|
{ target: 'hilanat', gloss: 'Fieber' },
|
||||||
|
{ target: 'kasakit', gloss: 'Schmerz' },
|
||||||
|
{ target: 'ulo', gloss: 'Kopf' },
|
||||||
|
{ target: 'tiyan', gloss: 'Bauch' },
|
||||||
|
{ target: 'doktor', gloss: 'Arzt / Ärztin' },
|
||||||
|
{ target: 'tubig', gloss: 'Wasser' },
|
||||||
|
{ target: 'Aduna kay hilanat?', gloss: 'Hast du Fieber?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Symptom zu Hilfe', prompt: 'Ordne Husten, Fieber und Schmerz je einer passenden Hilfe zu.', cue: 'Aduna kay hilanat? Magpahuway sa. Uminom og tubig.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Gesundheitskarten', text: 'Ziehe gedanklich acht Gesundheitswörter und bilde daraus vier kurze Hilfesätze.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Woche 5 - Intensivwiederholung II': {
|
||||||
|
learningGoals: [
|
||||||
|
'Gesundheit, Besuch, Fragen und Terminplanung gemischt reaktivieren.',
|
||||||
|
'Ähnliche Muster gezielt unterscheiden: Frage, Bitte, Angebot, Rat.',
|
||||||
|
'Unter leichtem Zeitdruck verständlich reagieren.',
|
||||||
|
'Fehlercluster aus Woche 5 für SRS und Wiederholung sichtbar machen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
|
||||||
|
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
|
||||||
|
{ target: 'Salamat sa pag-anhi.', gloss: 'Danke fürs Kommen.' },
|
||||||
|
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
|
||||||
|
{ target: 'Mas maayo na ka?', gloss: 'Geht es dir schon besser?' },
|
||||||
|
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann heute nicht.' },
|
||||||
|
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Mischrunde', prompt: 'Reagiere nacheinander auf Besuch, Krankheit, Termin und Verständnisproblem.', cue: 'Sulod lang. Niinom ka og tambal? Pwede ugma? Palihug ka mubalik?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Fehlercluster', text: 'Notiere nach der Übung drei Sätze, bei denen du gezögert hast, und wiederhole sie laut.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Woche 5 - Checkpoint': {
|
||||||
|
learningGoals: [
|
||||||
|
'Die Inhalte von Woche 5 diagnostisch sichern.',
|
||||||
|
'Besuch, Termine, Fragen und Gesundheit in freien Antworten kombinieren.',
|
||||||
|
'Schwache Muster für die nächste Wiederholungsphase markieren.',
|
||||||
|
'Eine kurze Besuchs- oder Pflegesituation ohne Vorlage lösen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
|
||||||
|
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
|
||||||
|
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann heute nicht.' },
|
||||||
|
{ target: 'Magpahuway sa.', gloss: 'Ruh dich erst einmal aus.' },
|
||||||
|
{ target: 'Sakit imong ulo?', gloss: 'Tut dein Kopf weh?' },
|
||||||
|
{ target: 'Kumusta ang biyahe?', gloss: 'Wie war die Reise?' },
|
||||||
|
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
|
||||||
|
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Checkpoint-Szene', prompt: 'Löse eine Szene mit Besuch, Termin und Gesundheitsfrage.', cue: 'Sulod lang. Magkita ta ugma? Sakit imong ulo?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Diagnose', text: 'Beantworte fünf freie Situationen und markiere alle Sätze, die nicht sofort kamen.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Unterwegs & Transport': {
|
||||||
|
learningGoals: [
|
||||||
|
'Nach Weg, Haltestelle, Fahrpreis und Ziel fragen.',
|
||||||
|
'Sagen, wohin man fährt und wo man aussteigen möchte.',
|
||||||
|
'Transportfragen mit Orts- und Preiswissen aus früheren Wochen verbinden.',
|
||||||
|
'Eine einfache Fahrt sprachlich organisieren.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Asa ang sakayan?', gloss: 'Wo ist die Haltestelle / Mitfahrstelle?' },
|
||||||
|
{ target: 'Moadto ko sa lungsod.', gloss: 'Ich fahre/gehe in die Stadt.' },
|
||||||
|
{ target: 'Hunong lang dinhi.', gloss: 'Bitte hier anhalten.' },
|
||||||
|
{ target: 'Pila ang plite?', gloss: 'Wie viel kostet die Fahrt?' },
|
||||||
|
{ target: 'Asa ko manaog?', gloss: 'Wo soll ich aussteigen?' },
|
||||||
|
{ target: 'Duol ra ba?', gloss: 'Ist es nah?' },
|
||||||
|
{ target: 'Layo pa ba?', gloss: 'Ist es noch weit?' },
|
||||||
|
{ target: 'Padulong ko sa balay.', gloss: 'Ich bin auf dem Weg nach Hause.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'moadto und padulong', text: 'moadto betont das Hingehen/Fahren, padulong beschreibt die Richtung.', example: 'Moadto ko sa lungsod. Padulong ko sa balay.' },
|
||||||
|
{ title: 'dinhi', text: 'dinhi bedeutet hier und ist beim Aussteigen oder Anhalten praktisch.', example: 'Hunong lang dinhi.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Weg erfragen', prompt: 'Frage nach Haltestelle, Preis und Ausstieg.', cue: 'Asa ang sakayan? Pila ang plite? Asa ko manaog?' },
|
||||||
|
{ title: 'Fahrt beschreiben', prompt: 'Sage, dass du in die Stadt und später nach Hause fährst.', cue: 'Moadto ko sa lungsod. Padulong ko sa balay.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Transportdialog', text: 'Sprich eine kurze Fahrt: Ziel, Preis, Ausstieg, Dank.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Wege & Verkehr': {
|
||||||
|
learningGoals: [
|
||||||
|
'Verkehrs- und Wegwörter aktiv abrufen.',
|
||||||
|
'Route, Fahrzeug und Halt in einfachen Sätzen beschreiben.',
|
||||||
|
'Frühere Richtungswörter mit Transport verbinden.',
|
||||||
|
'Eine kurze Route mit mindestens vier Stationen erklären.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'sakyanan', gloss: 'Fahrzeug / Auto' },
|
||||||
|
{ target: 'jeepney', gloss: 'Jeepney' },
|
||||||
|
{ target: 'dalan', gloss: 'Straße / Weg' },
|
||||||
|
{ target: 'hunong', gloss: 'Halt / anhalten' },
|
||||||
|
{ target: 'biyahe', gloss: 'Reise / Fahrt' },
|
||||||
|
{ target: 'tuo', gloss: 'rechts' },
|
||||||
|
{ target: 'wala', gloss: 'links' },
|
||||||
|
{ target: 'diretso', gloss: 'geradeaus' },
|
||||||
|
{ target: 'Duol ra.', gloss: 'Es ist nur nah.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Route beschreiben', prompt: 'Beschreibe eine einfache Route mit geradeaus, rechts und links.', cue: 'Diretso. Tuo. Wala. Duol ra.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Wegskizze', text: 'Erkläre den Weg von Zuhause zur Haltestelle in vier kurzen Sätzen.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Arbeit & Aufgaben': {
|
||||||
|
learningGoals: [
|
||||||
|
'Über Arbeit, Haushaltspflichten und To-dos sprechen.',
|
||||||
|
'Sagen, was man noch erledigen muss und ob man fertig ist.',
|
||||||
|
'Hilfe anbieten oder erbitten.',
|
||||||
|
'Eine Tagesaufgabe mit Zeit und Grund erklären.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Naa koy trabaho karon.', gloss: 'Ich habe heute Arbeit.' },
|
||||||
|
{ target: 'Aduna koy buhaton.', gloss: 'Ich habe etwas zu erledigen.' },
|
||||||
|
{ target: 'Human na ko.', gloss: 'Ich bin fertig.' },
|
||||||
|
{ target: 'Wala pa ko nahuman.', gloss: 'Ich bin noch nicht fertig.' },
|
||||||
|
{ target: 'Tabang ta.', gloss: 'Lass uns helfen.' },
|
||||||
|
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
|
||||||
|
{ target: 'Unahon nako ni.', gloss: 'Das mache ich zuerst.' },
|
||||||
|
{ target: 'Human sa paniudto.', gloss: 'Nach dem Mittagessen.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'naa koy / aduna koy', text: 'Beide Muster drücken aus, dass du etwas hast oder erledigen musst.', example: 'Naa koy trabaho. Aduna koy buhaton.' },
|
||||||
|
{ title: 'human na vs. wala pa', text: 'human na zeigt fertig, wala pa zeigt noch nicht.', example: 'Human na ko. Wala pa ko nahuman.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Tagesaufgabe', prompt: 'Erkläre, was du heute erledigen musst, was zuerst kommt und wann du fertig bist.', cue: 'Aduna koy buhaton. Unahon nako ni. Human na ko unya.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'To-do laut', text: 'Sprich drei Aufgaben: eine erledigt, eine offen, eine mit Hilfe.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Tätigkeiten & Organisation': {
|
||||||
|
learningGoals: [
|
||||||
|
'Organisationswortschatz für Alltag und Haushalt erweitern.',
|
||||||
|
'Listen, Aufgaben und Hilfe in kurzen Sätzen verwenden.',
|
||||||
|
'Zwischen Tätigkeit, Person und Reihenfolge unterscheiden.',
|
||||||
|
'Eine einfache To-do-Liste auf Bisaya sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'trabaho', gloss: 'Arbeit' },
|
||||||
|
{ target: 'buluhaton', gloss: 'Aufgabe / Tätigkeit' },
|
||||||
|
{ target: 'lista', gloss: 'Liste' },
|
||||||
|
{ target: 'tabang', gloss: 'Hilfe' },
|
||||||
|
{ target: 'una', gloss: 'zuerst' },
|
||||||
|
{ target: 'sunod', gloss: 'als Nächstes' },
|
||||||
|
{ target: 'human', gloss: 'fertig / danach' },
|
||||||
|
{ target: 'Kinsa ang motabang?', gloss: 'Wer hilft?' },
|
||||||
|
{ target: 'Unsa ang sunod?', gloss: 'Was kommt als Nächstes?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Organisation', prompt: 'Sage, was zuerst kommt, was danach kommt und wer hilft.', cue: 'Una kini. Sunod kana. Kinsa ang motabang?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Mini-Liste', text: 'Erstelle mündlich eine Liste mit drei Aufgaben und einer Bitte um Hilfe.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Freies Gespräch - Familie & Alltag': {
|
||||||
|
learningGoals: [
|
||||||
|
'Frühere Themen ohne enge Führung kombinieren.',
|
||||||
|
'Längere Alltagsantworten mit Familie, Zuhause, Befinden und Plan aufbauen.',
|
||||||
|
'Bei Unsicherheit Reparaturstrategien verwenden statt abzubrechen.',
|
||||||
|
'Mindestens fünf zusammenhängende Sätze frei sprechen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Maayo ra ko karon.', gloss: 'Mir geht es heute okay.' },
|
||||||
|
{ target: 'Naa mi sa balay.', gloss: 'Wir sind zuhause.' },
|
||||||
|
{ target: 'Nikaon na mi.', gloss: 'Wir haben schon gegessen.' },
|
||||||
|
{ target: 'Aduna koy buhaton unya.', gloss: 'Ich habe später etwas zu erledigen.' },
|
||||||
|
{ target: 'Magkita mi unya.', gloss: 'Wir sehen uns später.' },
|
||||||
|
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
|
||||||
|
{ target: 'Wala ko kasabot, palihug balik.', gloss: 'Ich verstehe nicht, bitte wiederhole es.' },
|
||||||
|
{ target: 'Amping kanunay.', gloss: 'Pass immer auf dich auf.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'mi für wir', text: 'mi nutzt du, wenn du über wir sprichst und die angesprochene Person nicht einschließt.', example: 'Naa mi sa balay. Nikaon na mi.' },
|
||||||
|
{ title: 'Freies Sprechen mit Ankern', text: 'Nutze bekannte Satzanker und füge nur kleine neue Teile hinzu.', example: 'Maayo ra ko karon. Aduna koy buhaton unya.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Fünf Sätze frei', prompt: 'Erzähle von deinem Tag mit Befinden, Zuhause, Essen, Plan und Nähe.', cue: 'Maayo ra ko karon. Naa mi sa balay. Nikaon na mi. Aduna koy buhaton unya. Gimingaw ko nimo.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Freie Aufnahme', text: 'Sprich 30 Sekunden frei und verwende mindestens einen Reparatursatz, wenn du stockst.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Spiralwiederholung - Wochen 1 bis 4': {
|
||||||
|
learningGoals: [
|
||||||
|
'Grundlagen aus den ersten vier Wochen breit reaktivieren.',
|
||||||
|
'Begrüßung, Essen, Zeit, Preise, Gefühle und Gesundheit mischen.',
|
||||||
|
'Alte Muster in neuen Kontexten wiederfinden.',
|
||||||
|
'Langzeitabruf stärker gewichten als neues Material.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||||
|
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
|
||||||
|
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
|
||||||
|
{ target: 'Tagpila ni?', gloss: 'Wie viel kostet das?' },
|
||||||
|
{ target: 'Karon', gloss: 'jetzt / heute' },
|
||||||
|
{ target: 'Ugma', gloss: 'morgen' },
|
||||||
|
{ target: 'Nalipay ko.', gloss: 'Ich freue mich / bin glücklich.' },
|
||||||
|
{ target: 'Sakit akong ulo.', gloss: 'Mein Kopf tut weh.' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Blockmix', prompt: 'Baue aus Begrüßung, Essen, Preis und Gesundheit eine Alltagsszene.', cue: 'Kumusta ka? Nikaon na ka? Tagpila ni? Sakit akong ulo.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Langzeitabruf', text: 'Ziehe zehn alte Wörter/Sätze zufällig und bilde daraus fünf kurze Dialogantworten.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Konflikte & Missverständnisse': {
|
||||||
|
learningGoals: [
|
||||||
|
'Missverständnisse höflich ansprechen und entschärfen.',
|
||||||
|
'Um Wiederholung, langsameres Sprechen oder Erklärung bitten.',
|
||||||
|
'Eigene Unsicherheit ausdrücken, ohne das Gespräch abzubrechen.',
|
||||||
|
'Eine kleine heikle Situation ruhig reparieren.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Wala ko kasabot.', gloss: 'Ich verstehe nicht.' },
|
||||||
|
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
|
||||||
|
{ target: 'Pwede nato istoryahan?', gloss: 'Können wir darüber sprechen?' },
|
||||||
|
{ target: 'Pasayloa ko.', gloss: 'Entschuldige mich.' },
|
||||||
|
{ target: 'Dili mao akong pasabot.', gloss: 'Das meinte ich nicht.' },
|
||||||
|
{ target: 'Sige lang.', gloss: 'Schon okay.' },
|
||||||
|
{ target: 'Salamat sa pagsabot.', gloss: 'Danke fürs Verständnis.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Reparieren statt abbrechen', text: 'Ein Reparatursatz hält das Gespräch offen, auch wenn du etwas nicht verstanden hast.', example: 'Wala ko kasabot. Palihug ka mubalik?' },
|
||||||
|
{ title: 'Weich entschärfen', text: 'Sige lang und Salamat sa pagsabot helfen, Spannung aus dem Gespräch zu nehmen.', example: 'Sige lang. Salamat sa pagsabot.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Missverständnis lösen', prompt: 'Sage, dass du nicht verstanden hast, bitte um Wiederholung und entschuldige dich.', cue: 'Wala ko kasabot. Palihug ka mubalik? Pasayloa ko.' },
|
||||||
|
{ title: 'Heikle Korrektur', prompt: 'Sage, dass du etwas nicht so gemeint hast, und danke fürs Verständnis.', cue: 'Dili mao akong pasabot. Salamat sa pagsabot.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Konfliktanker', text: 'Übe fünf Reparatursätze, bis du sie ohne Lesen sagen kannst.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Abschlusstest - Schnellstart': {
|
||||||
|
learningGoals: [
|
||||||
|
'Aktiven Grundwortschatz aus dem gesamten 6-Wochen-Schnellstart prüfen.',
|
||||||
|
'Wörter und kurze Sätze in beide Richtungen abrufen.',
|
||||||
|
'Ähnliche Muster sicher unterscheiden.',
|
||||||
|
'Schwächen für die nächste SRS-Wiederholung markieren.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Kumusta', gloss: 'Wie geht es / Hallo' },
|
||||||
|
{ target: 'bisita', gloss: 'Besuch / Gast' },
|
||||||
|
{ target: 'tambal', gloss: 'Medizin' },
|
||||||
|
{ target: 'plite', gloss: 'Fahrpreis' },
|
||||||
|
{ target: 'trabaho', gloss: 'Arbeit' },
|
||||||
|
{ target: 'buluhaton', gloss: 'Aufgabe' },
|
||||||
|
{ target: 'dalan', gloss: 'Straße / Weg' },
|
||||||
|
{ target: 'pamilya', gloss: 'Familie' },
|
||||||
|
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
|
||||||
|
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Aktiver Wortschatz', prompt: 'Wähle acht Wörter und bilde vier Alltagssätze daraus.', cue: 'bisita -> Sulod lang. tambal -> Niinom ka og tambal?' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Beide Richtungen', text: 'Übersetze zehn Einheiten Deutsch -> Bisaya und Bisaya -> Deutsch.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Abschlussprüfung - Schnellstart': {
|
||||||
|
learningGoals: [
|
||||||
|
'Den 6-Wochen-Schnellstart in Dialog, Abruf und freier Anwendung prüfen.',
|
||||||
|
'Besuch, Gesundheit, Transport, Arbeit und Familie zusammenführen.',
|
||||||
|
'Mehrere Sätze ohne starre Vorlage produzieren.',
|
||||||
|
'Vor dem Übergang in die Alltagsphase stabile Grundfähigkeit zeigen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'Sulod lang. Lingkod sa.', gloss: 'Komm herein. Setz dich erst einmal.' },
|
||||||
|
{ target: 'Magkita ta ugma. Unsa orasa?', gloss: 'Wir treffen uns morgen. Um wie viel Uhr?' },
|
||||||
|
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
|
||||||
|
{ target: 'Asa ang sakayan?', gloss: 'Wo ist die Haltestelle?' },
|
||||||
|
{ target: 'Pila ang plite?', gloss: 'Wie viel kostet die Fahrt?' },
|
||||||
|
{ target: 'Aduna koy buhaton karon.', gloss: 'Ich habe heute etwas zu erledigen.' },
|
||||||
|
{ target: 'Wala ko kasabot. Hinay-hinay lang.', gloss: 'Ich verstehe nicht. Bitte langsam.' },
|
||||||
|
{ target: 'Amping kanunay.', gloss: 'Pass immer auf dich auf.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Große Alltagsszene', prompt: 'Verbinde Besuch, Termin, Gesundheit und Transport in einem Mini-Dialog.', cue: 'Sulod lang. Magkita ta ugma? Niinom ka og tambal? Asa ang sakayan?' },
|
||||||
|
{ title: 'Freie Antwort', prompt: 'Antworte in mindestens fünf Sätzen auf: Was machst du heute und wen triffst du?', cue: 'Aduna koy buhaton karon. Magkita ta ugma. Amping kanunay.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Abschlussaufnahme', text: 'Sprich eine 45-Sekunden-Szene mit mindestens sechs Kernmustern aus Woche 1-6.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'Kulturelle Vertiefung im Familienalltag': {
|
||||||
|
learningGoals: [
|
||||||
|
'Sprache, Familiennähe und Respekt kulturell einordnen.',
|
||||||
|
'Indirekte Kommunikation als soziale Strategie verstehen.',
|
||||||
|
'Nähe, Hilfe und Ablehnung passend formulieren.',
|
||||||
|
'Kulturelles Verständnis in konkrete Antwortwahl übertragen.'
|
||||||
|
],
|
||||||
|
corePatterns: [
|
||||||
|
{ target: 'pakikisama', gloss: 'soziale Harmonie / gutes Miteinander' },
|
||||||
|
{ target: 'respeto', gloss: 'Respekt' },
|
||||||
|
{ target: 'amping', gloss: 'Pass auf dich auf' },
|
||||||
|
{ target: 'palihug', gloss: 'bitte' },
|
||||||
|
{ target: 'Dili lang sa karon.', gloss: 'Jetzt lieber nicht.' },
|
||||||
|
{ target: 'Sige lang.', gloss: 'Schon okay.' },
|
||||||
|
{ target: 'Tabang ta.', gloss: 'Lass uns helfen.' },
|
||||||
|
{ target: 'Salamat sa pagsabot.', gloss: 'Danke fürs Verständnis.' }
|
||||||
|
],
|
||||||
|
grammarFocus: [
|
||||||
|
{ title: 'Indirekt ist oft höflicher', text: 'In Familien- und Besuchskontexten kann eine weiche Formulierung natürlicher sein als ein direktes Nein.', example: 'Dili lang sa karon. Sunod na lang.' },
|
||||||
|
{ title: 'Sprache als Beziehungspflege', text: 'Viele Formeln transportieren nicht nur Inhalt, sondern Fürsorge und Nähe.', example: 'Amping kanunay. Tabang ta.' }
|
||||||
|
],
|
||||||
|
speakingPrompts: [
|
||||||
|
{ title: 'Kulturell passend reagieren', prompt: 'Lehne eine Einladung weich ab und bedanke dich fürs Verständnis.', cue: 'Dili lang sa karon. Salamat sa pagsabot.' },
|
||||||
|
{ title: 'Hilfe anbieten', prompt: 'Biete Hilfe an, ohne aufdringlich zu klingen.', cue: 'Tabang ta. Sige lang.' }
|
||||||
|
],
|
||||||
|
practicalTasks: [
|
||||||
|
{ title: 'Antwortwahl', text: 'Vergleiche drei direkte deutsche Antworten und formuliere sie weicher auf Bisaya.' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BISAYA_PHASE3_LESSONS = [
|
||||||
|
{ week: 5, day: 1, num: 44, type: 'conversation', title: 'Besuch & Gastfreundschaft', desc: 'Besuch empfangen, hereinbitten, etwas anbieten und nach der Reise fragen', targetMin: 24, targetScore: 80, review: false, cultural: 'Gastfreundschaft ist im philippinischen Familienalltag zentral und wird oft durch konkrete Fürsorge gezeigt.' },
|
||||||
|
{ week: 5, day: 1, num: 45, type: 'vocab', title: 'Besuch & Haushalt', desc: 'Haus-, Besuchs- und Haushaltswörter in kurzen Ortssätzen verwenden', targetMin: 22, targetScore: 85, review: true, cultural: null },
|
||||||
|
{ week: 5, day: 2, num: 46, type: 'grammar', title: 'Fragen im Alltag vertiefen', desc: 'Rückfragen, Folgefragen, Bedeutung und höfliches Nachhaken sicher kombinieren', targetMin: 26, targetScore: 78, review: true, cultural: null },
|
||||||
|
{ week: 5, day: 2, num: 47, type: 'conversation', title: 'Termine & Verabredungen', desc: 'Treffen planen, Uhrzeiten absprechen, zusagen und weich verschieben', targetMin: 24, targetScore: 80, review: false, cultural: null },
|
||||||
|
{ week: 5, day: 3, num: 48, type: 'review', title: 'Woche 5 - Intensivwiederholung I', desc: 'Besuch, Familie, Fürsorge und Terminplanung in schnellen Rollenwechseln mischen', targetMin: 34, targetScore: 82, review: false, cultural: null },
|
||||||
|
{ week: 5, day: 3, num: 49, type: 'vocab', title: 'Spiralwiederholung - Familie & Fürsorge', desc: 'Alte Familien- und Fürsorgemuster aktiv reaktivieren und in neue Szenen übertragen', targetMin: 24, targetScore: 85, review: true, cultural: null },
|
||||||
|
{ week: 5, day: 4, num: 50, type: 'conversation', title: 'Gesundheit im Alltag', desc: 'Beschwerden erfragen, Ruhe/Wasser/Medizin anbieten und nach Besserung fragen', targetMin: 26, targetScore: 80, review: false, cultural: null },
|
||||||
|
{ week: 5, day: 4, num: 51, type: 'vocab', title: 'Medikamente & Beschwerden', desc: 'Symptome, Körperwörter und Hilfewortschatz in Fürsorgesätzen verwenden', targetMin: 24, targetScore: 85, review: true, cultural: null },
|
||||||
|
{ week: 5, day: 5, num: 52, type: 'review', title: 'Woche 5 - Intensivwiederholung II', desc: 'Gesundheit, Besuch, Fragen und Terminplanung unter Abrufdruck kontrastieren', targetMin: 34, targetScore: 82, review: false, cultural: null },
|
||||||
|
{ week: 5, day: 5, num: 53, type: 'vocab', title: 'Woche 5 - Checkpoint', desc: 'Diagnostischer Checkpoint zu Besuch, Termin, Frageketten und Gesundheit', targetMin: 24, targetScore: 84, review: true, cultural: null },
|
||||||
|
{ week: 6, day: 1, num: 54, type: 'conversation', title: 'Unterwegs & Transport', desc: 'Nach Weg, Haltestelle, Fahrpreis, Ziel und Ausstieg fragen', targetMin: 26, targetScore: 80, review: false, cultural: null },
|
||||||
|
{ week: 6, day: 1, num: 55, type: 'vocab', title: 'Wege & Verkehr', desc: 'Verkehrs-, Richtungs- und Bewegungswortschatz in einfachen Routen verwenden', targetMin: 24, targetScore: 85, review: true, cultural: null },
|
||||||
|
{ week: 6, day: 2, num: 56, type: 'conversation', title: 'Arbeit & Aufgaben', desc: 'Über Arbeit, Pflichten, Reihenfolge, Fertigsein und Hilfe sprechen', targetMin: 26, targetScore: 80, review: false, cultural: null },
|
||||||
|
{ week: 6, day: 2, num: 57, type: 'vocab', title: 'Tätigkeiten & Organisation', desc: 'Aufgaben, Listen, Reihenfolge und Hilfe als To-do-Sprache festigen', targetMin: 24, targetScore: 85, review: true, cultural: null },
|
||||||
|
{ week: 6, day: 3, num: 58, type: 'conversation', title: 'Freies Gespräch - Familie & Alltag', desc: 'Fünf zusammenhängende Sätze zu Familie, Zuhause, Befinden und Tagesplan sprechen', targetMin: 30, targetScore: 78, review: false, cultural: null },
|
||||||
|
{ week: 6, day: 3, num: 59, type: 'review', title: 'Spiralwiederholung - Wochen 1 bis 4', desc: 'Begrüßung, Essen, Zeit, Preise, Gefühle und Gesundheit als Langzeitabruf mischen', targetMin: 34, targetScore: 84, review: false, cultural: null },
|
||||||
|
{ week: 6, day: 4, num: 60, type: 'conversation', title: 'Konflikte & Missverständnisse', desc: 'Missverständnisse höflich reparieren, um Wiederholung bitten und Spannung entschärfen', targetMin: 26, targetScore: 80, review: false, cultural: 'Ruhiger, indirekter Umgang hilft in heiklen Gesprächen oft mehr als direkte Korrektur.' },
|
||||||
|
{ week: 6, day: 4, num: 61, type: 'vocab', title: 'Abschlusstest - Schnellstart', desc: 'Aktiver Wortschatztest über den 6-Wochen-Schnellstart in beide Richtungen', targetMin: 24, targetScore: 84, review: true, cultural: null },
|
||||||
|
{ week: 6, day: 5, num: 62, type: 'review', title: 'Abschlussprüfung - Schnellstart', desc: 'Große Abschlussprüfung mit Besuch, Gesundheit, Transport, Arbeit und freier Anwendung', targetMin: 38, targetScore: 84, review: false, cultural: 'Die Abschlussphase prüft Verständlichkeit und Alltagsfähigkeit, nicht perfekte Grammatik.' },
|
||||||
|
{ week: 6, day: 5, num: 63, type: 'culture', title: 'Kulturelle Vertiefung im Familienalltag', desc: 'Respekt, Nähe, indirekte Kommunikation und Hilfe im Familienalltag einordnen', targetMin: 22, targetScore: 0, review: false, cultural: 'Sprache, Respekt und Familienrollen sind eng miteinander verbunden.' }
|
||||||
|
];
|
||||||
1036
backend/scripts/bisaya-course-phase4-extension.js
Normal file
1036
backend/scripts/bisaya-course-phase4-extension.js
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user