Implement member management enhancements; add bulk import functionality and duplicate checking based on geburtsdatum. Update API to support new fields and improve error handling for member data submissions. Refactor member-related components for better user experience and data validation.

This commit is contained in:
Torsten Schulz (local)
2025-11-05 14:34:31 +01:00
parent dd4691b462
commit 623a63c29f
21 changed files with 1765 additions and 513 deletions

View File

@@ -1,5 +1,5 @@
{ {
"date": "2025-11-05T12:46:44.995Z", "date": "2025-11-05T13:24:50.100Z",
"preset": "node-server", "preset": "node-server",
"framework": { "framework": {
"name": "nuxt", "name": "nuxt",

View File

@@ -1 +1 @@
{"id":"9438b35d-0d10-4203-a329-f7a1e287e2a1","timestamp":1762346796211} {"id":"8af76f9e-6e85-416c-9e2f-92c68e0dfd76","timestamp":1762349079882}

View File

@@ -72,16 +72,57 @@ async function writeMembers(members) {
return false; return false;
} }
} }
function normalizeDate(dateString) {
if (!dateString) return "";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return dateString.trim();
return date.toISOString().split("T")[0];
} catch (e) {
return dateString.trim();
}
}
function findDuplicateMember(members, firstName, lastName, geburtsdatum) {
const normalizedFirstName = (firstName || "").trim().toLowerCase();
const normalizedLastName = (lastName || "").trim().toLowerCase();
const normalizedDate = normalizeDate(geburtsdatum);
return members.find((m) => {
const mFirstName = (m.firstName || "").trim().toLowerCase();
const mLastName = (m.lastName || "").trim().toLowerCase();
const mDate = normalizeDate(m.geburtsdatum);
return mFirstName === normalizedFirstName && mLastName === normalizedLastName && mDate === normalizedDate && mDate !== "";
});
}
async function saveMember(memberData) { async function saveMember(memberData) {
const members = await readMembers(); const members = await readMembers();
if (memberData.id) { if (memberData.id) {
const index = members.findIndex((m) => m.id === memberData.id); const index = members.findIndex((m) => m.id === memberData.id);
if (index !== -1) { if (index !== -1) {
const duplicate = findDuplicateMember(
members.filter((m) => m.id !== memberData.id),
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
);
if (duplicate) {
throw new Error("Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.");
}
members[index] = { ...members[index], ...memberData }; members[index] = { ...members[index], ...memberData };
} else { } else {
throw new Error("Mitglied nicht gefunden"); throw new Error("Mitglied nicht gefunden");
} }
} else { } else {
if (memberData.firstName && memberData.lastName && memberData.geburtsdatum) {
const duplicate = findDuplicateMember(
members,
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
);
if (duplicate) {
throw new Error("Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.");
}
}
const newMember = { const newMember = {
...memberData, ...memberData,
id: randomUUID() id: randomUUID()
@@ -99,5 +140,5 @@ async function deleteMember(id) {
return true; return true;
} }
export { deleteMember as d, readMembers as r, saveMember as s }; export { deleteMember as d, normalizeDate as n, readMembers as r, saveMember as s, writeMembers as w };
//# sourceMappingURL=members.mjs.map //# sourceMappingURL=members.mjs.map

View File

@@ -1 +1 @@
{"version":3,"file":"members.mjs","sources":["../../../../server/utils/members.js"],"sourcesContent":null,"names":["fs"],"mappings":"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"} {"version":3,"file":"members.mjs","sources":["../../../../server/utils/members.js"],"sourcesContent":null,"names":["fs"],"mappings":"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"}

View File

@@ -296,7 +296,7 @@ const client_manifest = {
"module": true, "module": true,
"prefetch": true, "prefetch": true,
"preload": true, "preload": true,
"file": "oL_Xi1h-.js", "file": "CxKxaas7.js",
"name": "entry", "name": "entry",
"src": "node_modules/nuxt/dist/app/entry.js", "src": "node_modules/nuxt/dist/app/entry.js",
"isEntry": true, "isEntry": true,
@@ -306,14 +306,14 @@ const client_manifest = {
"node_modules/nuxt/dist/app/components/error-500.vue" "node_modules/nuxt/dist/app/components/error-500.vue"
], ],
"css": [ "css": [
"entry.CYvTzE5H.css" "entry.wEUbgGlA.css"
], ],
"assets": [ "assets": [
"Harheimer TC.CKfYAfp1.svg" "Harheimer TC.CKfYAfp1.svg"
] ]
}, },
"entry.CYvTzE5H.css": { "entry.wEUbgGlA.css": {
"file": "entry.CYvTzE5H.css", "file": "entry.wEUbgGlA.css",
"resourceType": "style", "resourceType": "style",
"prefetch": true, "prefetch": true,
"preload": true "preload": true
@@ -699,6 +699,20 @@ const client_manifest = {
"_Qy3ajxTk.js" "_Qy3ajxTk.js"
] ]
}, },
"pages/mitgliederbereich/api.vue": {
"resourceType": "script",
"module": true,
"prefetch": true,
"preload": true,
"file": "r4K7wPft.js",
"name": "api",
"src": "pages/mitgliederbereich/api.vue",
"isDynamicEntry": true,
"imports": [
"_Qy3ajxTk.js",
"node_modules/nuxt/dist/app/entry.js"
]
},
"pages/mitgliederbereich/index.vue": { "pages/mitgliederbereich/index.vue": {
"resourceType": "script", "resourceType": "script",
"module": true, "module": true,
@@ -721,7 +735,7 @@ const client_manifest = {
"module": true, "module": true,
"prefetch": true, "prefetch": true,
"preload": true, "preload": true,
"file": "CGzqx_GW.js", "file": "BKaEDYu3.js",
"name": "mitglieder", "name": "mitglieder",
"src": "pages/mitgliederbereich/mitglieder.vue", "src": "pages/mitgliederbereich/mitglieder.vue",
"isDynamicEntry": true, "isDynamicEntry": true,

View File

@@ -397,6 +397,9 @@ const unhead_k2P3m_ZDyjlr2mMYnoDPwavjsDN8hBlk9cFai0bbopU = /* @__PURE__ */ defin
function toArray(value) { function toArray(value) {
return Array.isArray(value) ? value : [value]; return Array.isArray(value) ? value : [value];
} }
const __nuxt_page_meta$8 = {
layout: "default"
};
const __nuxt_page_meta$7 = { const __nuxt_page_meta$7 = {
layout: "default" layout: "default"
}; };
@@ -430,7 +433,7 @@ const _routes = [
{ {
name: "login", name: "login",
path: "/login", path: "/login",
meta: __nuxt_page_meta$7 || {}, meta: __nuxt_page_meta$8 || {},
component: () => import('./login-P5Yg6Pmv.mjs') component: () => import('./login-P5Yg6Pmv.mjs')
}, },
{ {
@@ -466,7 +469,7 @@ const _routes = [
{ {
name: "cms", name: "cms",
path: "/cms", path: "/cms",
meta: { ...__nuxt_page_meta$6 || {}, ...{ "middleware": "auth" } }, meta: { ...__nuxt_page_meta$7 || {}, ...{ "middleware": "auth" } },
component: () => import('./index-CQoOFH9O.mjs') component: () => import('./index-CQoOFH9O.mjs')
}, },
{ {
@@ -503,7 +506,7 @@ const _routes = [
{ {
name: "cms-termine", name: "cms-termine",
path: "/cms/termine", path: "/cms/termine",
meta: { ...__nuxt_page_meta$5 || {}, ...{ "middleware": "auth" } }, meta: { ...__nuxt_page_meta$6 || {}, ...{ "middleware": "auth" } },
component: () => import('./termine-D0MhYqRR.mjs') component: () => import('./termine-D0MhYqRR.mjs')
}, },
{ {
@@ -579,7 +582,7 @@ const _routes = [
{ {
name: "cms-einstellungen", name: "cms-einstellungen",
path: "/cms/einstellungen", path: "/cms/einstellungen",
meta: { ...__nuxt_page_meta$4 || {}, ...{ "middleware": "auth" } }, meta: { ...__nuxt_page_meta$5 || {}, ...{ "middleware": "auth" } },
component: () => import('./einstellungen-D4Ua-Zgq.mjs') component: () => import('./einstellungen-D4Ua-Zgq.mjs')
}, },
{ {
@@ -622,6 +625,12 @@ const _routes = [
path: "/mannschaften/jugend", path: "/mannschaften/jugend",
component: () => import('./jugend-D2PYrcrQ.mjs') component: () => import('./jugend-D2PYrcrQ.mjs')
}, },
{
name: "mitgliederbereich-api",
path: "/mitgliederbereich/api",
meta: { ...__nuxt_page_meta$4 || {}, ...{ "middleware": "auth" } },
component: () => import('./api-2KvLR2u_.mjs')
},
{ {
name: "mitgliederbereich-news", name: "mitgliederbereich-news",
path: "/mitgliederbereich/news", path: "/mitgliederbereich/news",
@@ -665,7 +674,7 @@ const _routes = [
name: "mitgliederbereich-mitglieder", name: "mitgliederbereich-mitglieder",
path: "/mitgliederbereich/mitglieder", path: "/mitgliederbereich/mitglieder",
meta: { ...__nuxt_page_meta || {}, ...{ "middleware": "auth" } }, meta: { ...__nuxt_page_meta || {}, ...{ "middleware": "auth" } },
component: () => import('./mitglieder-BUT03wq4.mjs') component: () => import('./mitglieder-yWh5DTR0.mjs')
} }
]; ];
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g; const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g;
@@ -1930,6 +1939,23 @@ const _sfc_main$5 = {
}), }),
_: 1 _: 1
}, _parent)); }, _parent));
_push(`<div class="h-3 w-px bg-primary-700"></div>`);
_push(ssrRenderComponent(_component_NuxtLink, {
to: "/mitgliederbereich/api",
class: "px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all",
"active-class": "text-white bg-primary-600"
}, {
default: withCtx((_, _push2, _parent2, _scopeId) => {
if (_push2) {
_push2(` API-Dokumentation `);
} else {
return [
createTextVNode(" API-Dokumentation ")
];
}
}),
_: 1
}, _parent));
if (isAdmin.value) { if (isAdmin.value) {
_push(`<!--[--><div class="h-3 w-px bg-primary-700"></div><div class="relative inline-block"><button class="${ssrRenderClass([unref(route).path.startsWith("/cms") ? "text-white bg-primary-600" : "", "px-2.5 py-1 text-xs text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded transition-all flex items-center"])}"> CMS `); _push(`<!--[--><div class="h-3 w-px bg-primary-700"></div><div class="relative inline-block"><button class="${ssrRenderClass([unref(route).path.startsWith("/cms") ? "text-white bg-primary-600" : "", "px-2.5 py-1 text-xs text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded transition-all flex items-center"])}"> CMS `);
_push(ssrRenderComponent(unref(ChevronDown), { _push(ssrRenderComponent(unref(ChevronDown), {

View File

@@ -1 +1 @@
{"version":3,"file":"server.mjs","sources":["../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Ffetch.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fglobal-polyfills.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fnuxt.config.mjs","../../../../node_modules/nuxt/dist/app/nuxt.js","../../../../node_modules/nuxt/dist/app/components/injections.js","../../../../node_modules/nuxt/dist/app/utils.js","../../../../node_modules/nuxt/dist/app/composables/router.js","../../../../node_modules/nuxt/dist/app/composables/error.js","../../../../node_modules/nuxt/dist/app/composables/manifest.js","../../../../node_modules/nuxt/dist/app/composables/payload.js","../../../../node_modules/@pinia/nuxt/dist/runtime/payload-plugin.js","../../../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js","../../../../node_modules/nuxt/dist/pages/runtime/utils.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Froutes.mjs","../../../../node_modules/nuxt/dist/app/components/utils.js","../../../../node_modules/nuxt/dist/pages/runtime/router.options.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Frouter.options.mjs","../../../../node_modules/nuxt/dist/pages/runtime/validate.js","../../../../stores/auth.js","../../../../middleware/auth.global.js","../../../../node_modules/nuxt/dist/app/middleware/manifest-route-rule.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fmiddleware.mjs","../../../../node_modules/nuxt/dist/pages/runtime/plugins/router.js","../../../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js","../../../../node_modules/nuxt/dist/app/components/server-placeholder.js","../../../../node_modules/nuxt/dist/app/components/client-only.js","../../../../node_modules/nuxt/dist/app/components/nuxt-link.js","../../../../node_modules/@pinia/nuxt/dist/runtime/plugin.vue3.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fcomponents.plugin.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fplugins.server.mjs","../../../../node_modules/nuxt/dist/app/components/route-provider.js","../../../../node_modules/nuxt/dist/pages/runtime/page.js","../../../../assets/images/logos/Harheimer TC.svg","../../../../components/Navigation.vue","../../../../components/Footer.vue","../../../../components/ModalDialog.vue","../../../../app.vue","../../../../node_modules/nuxt/dist/app/components/nuxt-error-page.vue","../../../../node_modules/nuxt/dist/app/components/nuxt-root.vue","../../../../node_modules/nuxt/dist/app/entry.js"],"sourcesContent":null,"names":["$fetch","plugin","provide","plugins","createH3Error","createRadixRouter","login1RYyYL8mxx17qR_nmdKvywxx7lKOLXMFu8pTLfvTLYwMeta","indexBAhz9QmEiLA6QGDd6cSKraFX9E0RhZLvBUZorkJgAVkMeta","termineaIqWCm431cOVikspmAA3_6UhrHiffuFLM_t95ydtEd4Meta","einstellungen3Fhl_VQPzY4eklctozY70Drhj8cWMt_FVlyEKQh9aAAMeta","newsPHTXuxdDl4fgPHQQbukE_XsTEemyPOWVSYs64f8qjC4Meta","indexqVbusfljIJ04j42RIA_KP4bSP7XytQIXdqcVfR3kPUAMeta","profilyVBy_UvZ8KvchY44_0SJbB0NHOad6MC_S9C8wfDdUWEMeta","mitglieder9TWncRC_sSS_zPWzxBDlRiNjjFojXSJny_uernMe_tAMeta","__executeAsync","createRouter","entry","payload_plugin_1_bEQpMjikuQhbV8UJ0PxUqmSvPdmV1jDa5DURnKW4M","router_GNCWhvtYfLTYRZZ135CdFAEjxdMexN0ixiUYCAN_tpw","plugin_vue3_CQ_pO3THrTGIeYc0dvC91V75hY8qpo9B_8yZzOW5SFs","useRoute","_ssrRenderAttrs","_mergeProps","_push","_parent","_ssrRenderAttr","_imports_0","_createVNode","_ssrRenderClass","_unref","_ssrRenderList","_ssrInterpolate","_createTextVNode","_toDisplayString","useRouter","_ssrRenderComponent","showError","ErrorComponent","RootComponent"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,20,21,22,23,24,25,26,27,28,29,30,31,37,38,39]} {"version":3,"file":"server.mjs","sources":["../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Ffetch.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fglobal-polyfills.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fnuxt.config.mjs","../../../../node_modules/nuxt/dist/app/nuxt.js","../../../../node_modules/nuxt/dist/app/components/injections.js","../../../../node_modules/nuxt/dist/app/utils.js","../../../../node_modules/nuxt/dist/app/composables/router.js","../../../../node_modules/nuxt/dist/app/composables/error.js","../../../../node_modules/nuxt/dist/app/composables/manifest.js","../../../../node_modules/nuxt/dist/app/composables/payload.js","../../../../node_modules/@pinia/nuxt/dist/runtime/payload-plugin.js","../../../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js","../../../../node_modules/nuxt/dist/pages/runtime/utils.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Froutes.mjs","../../../../node_modules/nuxt/dist/app/components/utils.js","../../../../node_modules/nuxt/dist/pages/runtime/router.options.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Frouter.options.mjs","../../../../node_modules/nuxt/dist/pages/runtime/validate.js","../../../../stores/auth.js","../../../../middleware/auth.global.js","../../../../node_modules/nuxt/dist/app/middleware/manifest-route-rule.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fmiddleware.mjs","../../../../node_modules/nuxt/dist/pages/runtime/plugins/router.js","../../../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js","../../../../node_modules/nuxt/dist/app/components/server-placeholder.js","../../../../node_modules/nuxt/dist/app/components/client-only.js","../../../../node_modules/nuxt/dist/app/components/nuxt-link.js","../../../../node_modules/@pinia/nuxt/dist/runtime/plugin.vue3.js","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fcomponents.plugin.mjs","../../../../virtual:nuxt:%2Fmnt%2Fshare%2Ftorsten%2FPrograms%2Fharheimertc%2Fnode_modules%2F.cache%2Fnuxt%2F.nuxt%2Fplugins.server.mjs","../../../../node_modules/nuxt/dist/app/components/route-provider.js","../../../../node_modules/nuxt/dist/pages/runtime/page.js","../../../../assets/images/logos/Harheimer TC.svg","../../../../components/Navigation.vue","../../../../components/Footer.vue","../../../../components/ModalDialog.vue","../../../../app.vue","../../../../node_modules/nuxt/dist/app/components/nuxt-error-page.vue","../../../../node_modules/nuxt/dist/app/components/nuxt-root.vue","../../../../node_modules/nuxt/dist/app/entry.js"],"sourcesContent":null,"names":["$fetch","plugin","provide","plugins","createH3Error","createRadixRouter","login1RYyYL8mxx17qR_nmdKvywxx7lKOLXMFu8pTLfvTLYwMeta","indexBAhz9QmEiLA6QGDd6cSKraFX9E0RhZLvBUZorkJgAVkMeta","termineaIqWCm431cOVikspmAA3_6UhrHiffuFLM_t95ydtEd4Meta","einstellungen3Fhl_VQPzY4eklctozY70Drhj8cWMt_FVlyEKQh9aAAMeta","apis3_Iku1IZkQQ3FilRpoRZFd20ZCr3RAgIorGyDHhuNIMeta","newsPHTXuxdDl4fgPHQQbukE_XsTEemyPOWVSYs64f8qjC4Meta","indexqVbusfljIJ04j42RIA_KP4bSP7XytQIXdqcVfR3kPUAMeta","profilyVBy_UvZ8KvchY44_0SJbB0NHOad6MC_S9C8wfDdUWEMeta","mitglieder9TWncRC_sSS_zPWzxBDlRiNjjFojXSJny_uernMe_tAMeta","__executeAsync","createRouter","entry","payload_plugin_1_bEQpMjikuQhbV8UJ0PxUqmSvPdmV1jDa5DURnKW4M","router_GNCWhvtYfLTYRZZ135CdFAEjxdMexN0ixiUYCAN_tpw","plugin_vue3_CQ_pO3THrTGIeYc0dvC91V75hY8qpo9B_8yZzOW5SFs","useRoute","_ssrRenderAttrs","_mergeProps","_push","_parent","_ssrRenderAttr","_imports_0","_createVNode","_ssrRenderClass","_unref","_ssrRenderList","_ssrInterpolate","_createTextVNode","_toDisplayString","useRouter","_ssrRenderComponent","showError","ErrorComponent","RootComponent"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,20,21,22,23,24,25,26,27,28,29,30,31,37,38,39]}

View File

@@ -4,9 +4,9 @@ const styles = {
"node_modules/nuxt/dist/app/components/error-500.vue": () => import('./error-500-styles.Dccc6iq5.mjs').then(interopDefault), "node_modules/nuxt/dist/app/components/error-500.vue": () => import('./error-500-styles.Dccc6iq5.mjs').then(interopDefault),
"components/Hero.vue": () => import('./Hero-styles.CNXkzfUC.mjs').then(interopDefault), "components/Hero.vue": () => import('./Hero-styles.CNXkzfUC.mjs').then(interopDefault),
"components/PublicNews.vue": () => import('./PublicNews-styles.Muc48wAK.mjs').then(interopDefault), "components/PublicNews.vue": () => import('./PublicNews-styles.Muc48wAK.mjs').then(interopDefault),
"components/Hero.vue?vue&type=style&index=0&scoped=28200092&lang.css": () => import('./Hero-styles.CNXkzfUC.mjs').then(interopDefault),
"node_modules/nuxt/dist/app/components/error-404.vue?vue&type=style&index=0&scoped=b728498f&lang.css": () => import('./error-404-styles.BsF5Lbhq.mjs').then(interopDefault), "node_modules/nuxt/dist/app/components/error-404.vue?vue&type=style&index=0&scoped=b728498f&lang.css": () => import('./error-404-styles.BsF5Lbhq.mjs').then(interopDefault),
"node_modules/nuxt/dist/app/components/error-500.vue?vue&type=style&index=0&scoped=70d84538&lang.css": () => import('./error-500-styles.Dccc6iq5.mjs').then(interopDefault), "node_modules/nuxt/dist/app/components/error-500.vue?vue&type=style&index=0&scoped=70d84538&lang.css": () => import('./error-500-styles.Dccc6iq5.mjs').then(interopDefault),
"components/Hero.vue?vue&type=style&index=0&scoped=28200092&lang.css": () => import('./Hero-styles.CNXkzfUC.mjs').then(interopDefault),
"components/PublicNews.vue?vue&type=style&index=0&scoped=c57f605c&lang.css": () => import('./PublicNews-styles.Muc48wAK.mjs').then(interopDefault) "components/PublicNews.vue?vue&type=style&index=0&scoped=c57f605c&lang.css": () => import('./PublicNews-styles.Muc48wAK.mjs').then(interopDefault)
}; };

View File

@@ -4309,7 +4309,7 @@ function _expandFromEnv(value) {
const _inlineRuntimeConfig = { const _inlineRuntimeConfig = {
"app": { "app": {
"baseURL": "/", "baseURL": "/",
"buildId": "9438b35d-0d10-4203-a329-f7a1e287e2a1", "buildId": "8af76f9e-6e85-416c-9e2f-92c68e0dfd76",
"buildAssetsDir": "/_nuxt/", "buildAssetsDir": "/_nuxt/",
"cdnURL": "" "cdnURL": ""
}, },
@@ -4763,663 +4763,670 @@ const plugins = [
]; ];
const assets = { const assets = {
"/documents/Tischtennisregeln light.pdf": { "/images/club_about_us.png": {
"type": "application/pdf", "type": "image/png",
"etag": "\"5177b-y/88q2+Y3RRechJMqWhse21KRdQ\"", "etag": "\"202e56-s4fLsHEgoAgKJeBRuI1qxPmqHV0\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 333691, "size": 2109014,
"path": "../public/documents/Tischtennisregeln light.pdf" "path": "../public/images/club_about_us.png"
},
"/documents/beitrittserklärung_template.pdf": {
"type": "application/pdf",
"etag": "\"6ac0-KgukOaJtKwdNaJOX7uoc6iYn7og\"",
"mtime": "2025-11-05T12:46:41.630Z",
"size": 27328,
"path": "../public/documents/beitrittserklärung_template.pdf"
},
"/documents/satzung.pdf": {
"type": "application/pdf",
"etag": "\"5c7cf-L0A3nT8D24T9sD57FFbij3QRpzw\"",
"mtime": "2025-11-05T12:46:41.630Z",
"size": 378831,
"path": "../public/documents/satzung.pdf"
}, },
"/data/mannschaften.csv": { "/data/mannschaften.csv": {
"type": "text/csv; charset=utf-8", "type": "text/csv; charset=utf-8",
"etag": "\"858-l94GKn8Q0I5RQnhrM0ZPJsYUmcw\"", "etag": "\"858-l94GKn8Q0I5RQnhrM0ZPJsYUmcw\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 2136, "size": 2136,
"path": "../public/data/mannschaften.csv" "path": "../public/data/mannschaften.csv"
}, },
"/data/spielplan.csv": { "/data/spielplan.csv": {
"type": "text/csv; charset=utf-8", "type": "text/csv; charset=utf-8",
"etag": "\"8d38-QOShwHtHkSWtRusSJQlU5hXJcmk\"", "etag": "\"8d38-QOShwHtHkSWtRusSJQlU5hXJcmk\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 36152, "size": 36152,
"path": "../public/data/spielplan.csv" "path": "../public/data/spielplan.csv"
}, },
"/data/spielsysteme.csv": { "/data/spielsysteme.csv": {
"type": "text/csv; charset=utf-8", "type": "text/csv; charset=utf-8",
"etag": "\"9bc-4npLrNHYClsD0TKV5vSifxitfV0\"", "etag": "\"9bc-4npLrNHYClsD0TKV5vSifxitfV0\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 2492, "size": 2492,
"path": "../public/data/spielsysteme.csv" "path": "../public/data/spielsysteme.csv"
}, },
"/data/termine.csv": { "/data/termine.csv": {
"type": "text/csv; charset=utf-8", "type": "text/csv; charset=utf-8",
"etag": "\"a3-/pLg8n6Q4Adh8q/vZuoBiMsPHKY\"", "etag": "\"a3-/pLg8n6Q4Adh8q/vZuoBiMsPHKY\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 163, "size": 163,
"path": "../public/data/termine.csv" "path": "../public/data/termine.csv"
}, },
"/data/vereinsmeisterschaften.csv": { "/data/vereinsmeisterschaften.csv": {
"type": "text/csv; charset=utf-8", "type": "text/csv; charset=utf-8",
"etag": "\"99b-ovCRxbAET3nNRs52LvINNjQLWPY\"", "etag": "\"99b-ovCRxbAET3nNRs52LvINNjQLWPY\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 2459, "size": 2459,
"path": "../public/data/vereinsmeisterschaften.csv" "path": "../public/data/vereinsmeisterschaften.csv"
}, },
"/images/club_about_us.png": { "/documents/Tischtennisregeln light.pdf": {
"type": "image/png",
"etag": "\"202e56-s4fLsHEgoAgKJeBRuI1qxPmqHV0\"",
"mtime": "2025-11-05T12:46:41.629Z",
"size": 2109014,
"path": "../public/images/club_about_us.png"
},
"/spielplaene/1. Mannschaft 20252026.pdf": {
"type": "application/pdf", "type": "application/pdf",
"etag": "\"64c6-+477M+gD/spwpWR9NO/tMJ/inCc\"", "etag": "\"5177b-y/88q2+Y3RRechJMqWhse21KRdQ\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 25798, "size": 333691,
"path": "../public/spielplaene/1. Mannschaft 20252026.pdf" "path": "../public/documents/Tischtennisregeln light.pdf"
}, },
"/spielplaene/2. Mannschaft 20252026.pdf": { "/documents/beitrittserklärung_template.pdf": {
"type": "application/pdf", "type": "application/pdf",
"etag": "\"5bfa-DRJMHLV15iss67lEISoGqSYmZjE\"", "etag": "\"6ac0-KgukOaJtKwdNaJOX7uoc6iYn7og\"",
"mtime": "2025-11-05T12:46:41.630Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 23546, "size": 27328,
"path": "../public/spielplaene/2. Mannschaft 20252026.pdf" "path": "../public/documents/beitrittserklärung_template.pdf"
}, },
"/spielplaene/3. Mannschaft 20252026.pdf": { "/documents/satzung.pdf": {
"type": "application/pdf", "type": "application/pdf",
"etag": "\"7447-w933CPQdXhkWJ2AZOVdY0UgJnPo\"", "etag": "\"5c7cf-L0A3nT8D24T9sD57FFbij3QRpzw\"",
"mtime": "2025-11-05T12:46:41.630Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 29767, "size": 378831,
"path": "../public/spielplaene/3. Mannschaft 20252026.pdf" "path": "../public/documents/satzung.pdf"
},
"/spielplaene/4. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"6a9b-4TPGn1yQlFUMRj7oB43SN//Np9o\"",
"mtime": "2025-11-05T12:46:41.630Z",
"size": 27291,
"path": "../public/spielplaene/4. Mannschaft 20252026.pdf"
},
"/spielplaene/5. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"6523-5VUfCMaoiNhcwHhptHHTVJ3lSwQ\"",
"mtime": "2025-11-05T12:46:41.630Z",
"size": 25891,
"path": "../public/spielplaene/5. Mannschaft 20252026.pdf"
},
"/spielplaene/Jugend 11 20252026.pdf": {
"type": "application/pdf",
"etag": "\"52e9-3Rrk9UKUxPh80pBJ0w9oLVbe5dA\"",
"mtime": "2025-11-05T12:46:41.630Z",
"size": 21225,
"path": "../public/spielplaene/Jugend 11 20252026.pdf"
}, },
"/_nuxt/48ve60fm.js": { "/_nuxt/48ve60fm.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"557-e04q6bcr4Wja5BTiX/uCY9jcaOk\"", "etag": "\"557-e04q6bcr4Wja5BTiX/uCY9jcaOk\"",
"mtime": "2025-11-05T12:46:41.624Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 1367, "size": 1367,
"path": "../public/_nuxt/48ve60fm.js" "path": "../public/_nuxt/48ve60fm.js"
}, },
"/_nuxt/6EY4_GXp.js": { "/_nuxt/6EY4_GXp.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"19e-CzjelZROfgCPWyXIjP0DPtd+UHQ\"", "etag": "\"19e-CzjelZROfgCPWyXIjP0DPtd+UHQ\"",
"mtime": "2025-11-05T12:46:41.624Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 414, "size": 414,
"path": "../public/_nuxt/6EY4_GXp.js" "path": "../public/_nuxt/6EY4_GXp.js"
}, },
"/_nuxt/9IiqlUD-.js": { "/_nuxt/9IiqlUD-.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2835-WiVWi4XZDT1Nsy21w92+uGqrUhI\"", "etag": "\"2835-WiVWi4XZDT1Nsy21w92+uGqrUhI\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 10293, "size": 10293,
"path": "../public/_nuxt/9IiqlUD-.js" "path": "../public/_nuxt/9IiqlUD-.js"
}, },
"/_nuxt/B0O50Q40.js": { "/_nuxt/B0O50Q40.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2346-VOt5BwQrjzVpCVYv0biaieLYXbw\"", "etag": "\"2346-VOt5BwQrjzVpCVYv0biaieLYXbw\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 9030, "size": 9030,
"path": "../public/_nuxt/B0O50Q40.js" "path": "../public/_nuxt/B0O50Q40.js"
}, },
"/_nuxt/B1VMaWHv.js": { "/_nuxt/B1VMaWHv.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"16a7-ABFT0Hkz1mWSRL8qDQgQuhCoswc\"", "etag": "\"16a7-ABFT0Hkz1mWSRL8qDQgQuhCoswc\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 5799, "size": 5799,
"path": "../public/_nuxt/B1VMaWHv.js" "path": "../public/_nuxt/B1VMaWHv.js"
}, },
"/_nuxt/B4mSF5Ac.js": { "/_nuxt/B4mSF5Ac.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"185-hHs3mU4qOcQAkGQaPrUYGaG0yao\"", "etag": "\"185-hHs3mU4qOcQAkGQaPrUYGaG0yao\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 389, "size": 389,
"path": "../public/_nuxt/B4mSF5Ac.js" "path": "../public/_nuxt/B4mSF5Ac.js"
}, },
"/_nuxt/B9LKmM6w.js": { "/_nuxt/B9LKmM6w.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2970-DQ16XS35U2+PB0IaZFhNA7g4mJk\"", "etag": "\"2970-DQ16XS35U2+PB0IaZFhNA7g4mJk\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.321Z",
"size": 10608, "size": 10608,
"path": "../public/_nuxt/B9LKmM6w.js" "path": "../public/_nuxt/B9LKmM6w.js"
}, },
"/_nuxt/B9lUQxCj.js": { "/_nuxt/B9lUQxCj.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"c21-cIS5lR8JDKJkr1N/Y8gKIIRgLqI\"", "etag": "\"c21-cIS5lR8JDKJkr1N/Y8gKIIRgLqI\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3105, "size": 3105,
"path": "../public/_nuxt/B9lUQxCj.js" "path": "../public/_nuxt/B9lUQxCj.js"
}, },
"/_nuxt/BC4PNGtJ.js": { "/_nuxt/BC4PNGtJ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"175-33lu59Ps/+kwbPv/hVeUdrq4wmI\"", "etag": "\"175-33lu59Ps/+kwbPv/hVeUdrq4wmI\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 373, "size": 373,
"path": "../public/_nuxt/BC4PNGtJ.js" "path": "../public/_nuxt/BC4PNGtJ.js"
}, },
"/_nuxt/BIgEmhqk.js": { "/_nuxt/BIgEmhqk.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"3513-NZ/G+tsRsF+o3kKxOyYiTJ+ozJ0\"", "etag": "\"3513-NZ/G+tsRsF+o3kKxOyYiTJ+ozJ0\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 13587, "size": 13587,
"path": "../public/_nuxt/BIgEmhqk.js" "path": "../public/_nuxt/BIgEmhqk.js"
}, },
"/_nuxt/BKaEDYu3.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"5916-HkP5iPO4XP/4n2fQHChYeSwJHKc\"",
"mtime": "2025-11-05T13:24:46.322Z",
"size": 22806,
"path": "../public/_nuxt/BKaEDYu3.js"
},
"/_nuxt/BOhpfOYZ.js": { "/_nuxt/BOhpfOYZ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"b10-T1pZGL5tW1rfqWHftATE5VhNr1g\"", "etag": "\"b10-T1pZGL5tW1rfqWHftATE5VhNr1g\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 2832, "size": 2832,
"path": "../public/_nuxt/BOhpfOYZ.js" "path": "../public/_nuxt/BOhpfOYZ.js"
}, },
"/_nuxt/BQ1JAN-t.js": { "/_nuxt/BQ1JAN-t.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"4d4-D2c9LzMtu3t/oTdiDqO210OhOq0\"", "etag": "\"4d4-D2c9LzMtu3t/oTdiDqO210OhOq0\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 1236, "size": 1236,
"path": "../public/_nuxt/BQ1JAN-t.js" "path": "../public/_nuxt/BQ1JAN-t.js"
}, },
"/_nuxt/BRIjNg-H.js": { "/_nuxt/BRIjNg-H.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2820-w8/G0Dy6wRFecNHJSR+NwEJ2AeE\"", "etag": "\"2820-w8/G0Dy6wRFecNHJSR+NwEJ2AeE\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 10272, "size": 10272,
"path": "../public/_nuxt/BRIjNg-H.js" "path": "../public/_nuxt/BRIjNg-H.js"
}, },
"/_nuxt/BSgvdOgQ.js": { "/_nuxt/BSgvdOgQ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1827-zMnsckCG+udmw6Jb4QbWFvVU40I\"", "etag": "\"1827-zMnsckCG+udmw6Jb4QbWFvVU40I\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 6183, "size": 6183,
"path": "../public/_nuxt/BSgvdOgQ.js" "path": "../public/_nuxt/BSgvdOgQ.js"
}, },
"/_nuxt/BVRiFo7f.js": { "/_nuxt/BVRiFo7f.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"dbf-bs5Lp4co8JtdiCR4NBZg2xEEpZE\"", "etag": "\"dbf-bs5Lp4co8JtdiCR4NBZg2xEEpZE\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3519, "size": 3519,
"path": "../public/_nuxt/BVRiFo7f.js" "path": "../public/_nuxt/BVRiFo7f.js"
}, },
"/_nuxt/BWE4JIU7.js": { "/_nuxt/BWE4JIU7.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"d97-Mz4vlsBagIqTc5OFwjogM6kfacU\"", "etag": "\"d97-Mz4vlsBagIqTc5OFwjogM6kfacU\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3479, "size": 3479,
"path": "../public/_nuxt/BWE4JIU7.js" "path": "../public/_nuxt/BWE4JIU7.js"
}, },
"/_nuxt/BWWcyQAZ.js": { "/_nuxt/BWWcyQAZ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"685-pCeqOgBg8QXGBjOkvDQ5Miivcwk\"", "etag": "\"685-pCeqOgBg8QXGBjOkvDQ5Miivcwk\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 1669, "size": 1669,
"path": "../public/_nuxt/BWWcyQAZ.js" "path": "../public/_nuxt/BWWcyQAZ.js"
}, },
"/_nuxt/BeFU4xfJ.js": { "/_nuxt/BeFU4xfJ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1744-/7IohGje8DDLlxCyMiEkw3Thq+I\"", "etag": "\"1744-/7IohGje8DDLlxCyMiEkw3Thq+I\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 5956, "size": 5956,
"path": "../public/_nuxt/BeFU4xfJ.js" "path": "../public/_nuxt/BeFU4xfJ.js"
}, },
"/_nuxt/Bg-xR82b.js": { "/_nuxt/Bg-xR82b.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1860-q2IZycL1/cXKqiR+YJN8gMIGQ70\"", "etag": "\"1860-q2IZycL1/cXKqiR+YJN8gMIGQ70\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 6240, "size": 6240,
"path": "../public/_nuxt/Bg-xR82b.js" "path": "../public/_nuxt/Bg-xR82b.js"
}, },
"/_nuxt/BihH_yuU.js": { "/_nuxt/BihH_yuU.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"dbb-d//fGgPcM4CQDXXuyaBVA0etrcE\"", "etag": "\"dbb-d//fGgPcM4CQDXXuyaBVA0etrcE\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3515, "size": 3515,
"path": "../public/_nuxt/BihH_yuU.js" "path": "../public/_nuxt/BihH_yuU.js"
}, },
"/_nuxt/BteKZQ9T.js": { "/_nuxt/BteKZQ9T.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1ea-kmrGdt5SPmt15EiBI7kR9gXMQM0\"", "etag": "\"1ea-kmrGdt5SPmt15EiBI7kR9gXMQM0\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 490, "size": 490,
"path": "../public/_nuxt/BteKZQ9T.js" "path": "../public/_nuxt/BteKZQ9T.js"
}, },
"/_nuxt/BuN7Api-.js": { "/_nuxt/BuN7Api-.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"dcb-vEM3YBEgZbfkrV6Gw0ZoyLCb21I\"", "etag": "\"dcb-vEM3YBEgZbfkrV6Gw0ZoyLCb21I\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3531, "size": 3531,
"path": "../public/_nuxt/BuN7Api-.js" "path": "../public/_nuxt/BuN7Api-.js"
}, },
"/_nuxt/BzVlU_Az.js": { "/_nuxt/BzVlU_Az.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"3b44-YIew5nSdOke4bV7T76Jp4fRwf4k\"", "etag": "\"3b44-YIew5nSdOke4bV7T76Jp4fRwf4k\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 15172, "size": 15172,
"path": "../public/_nuxt/BzVlU_Az.js" "path": "../public/_nuxt/BzVlU_Az.js"
}, },
"/_nuxt/C0hTJwq1.js": { "/_nuxt/C0hTJwq1.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1588-AjSEZ5SBctJSg8q99Sy8Zj2YxXY\"", "etag": "\"1588-AjSEZ5SBctJSg8q99Sy8Zj2YxXY\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 5512, "size": 5512,
"path": "../public/_nuxt/C0hTJwq1.js" "path": "../public/_nuxt/C0hTJwq1.js"
}, },
"/_nuxt/C27DLx1c.js": { "/_nuxt/C27DLx1c.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"19ec-tkDTgwwh9gT7TzWUiNpwZMn2Uhc\"", "etag": "\"19ec-tkDTgwwh9gT7TzWUiNpwZMn2Uhc\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 6636, "size": 6636,
"path": "../public/_nuxt/C27DLx1c.js" "path": "../public/_nuxt/C27DLx1c.js"
}, },
"/_nuxt/C5SyyWEb.js": { "/_nuxt/C5SyyWEb.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2a5-06iX+CL3i0ysaqW9nu7Eg2YzDhQ\"", "etag": "\"2a5-06iX+CL3i0ysaqW9nu7Eg2YzDhQ\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 677, "size": 677,
"path": "../public/_nuxt/C5SyyWEb.js" "path": "../public/_nuxt/C5SyyWEb.js"
}, },
"/_nuxt/C8kQt0fa.js": { "/_nuxt/C8kQt0fa.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"197-7X99z1xphxry8OnMwU7Ofs/uE0Q\"", "etag": "\"197-7X99z1xphxry8OnMwU7Ofs/uE0Q\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 407, "size": 407,
"path": "../public/_nuxt/C8kQt0fa.js" "path": "../public/_nuxt/C8kQt0fa.js"
}, },
"/_nuxt/CED8cBGM.js": { "/_nuxt/CED8cBGM.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1c75-5CrjjdvSrpIsV0jgPgmLQ/Ag8Kw\"", "etag": "\"1c75-5CrjjdvSrpIsV0jgPgmLQ/Ag8Kw\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 7285, "size": 7285,
"path": "../public/_nuxt/CED8cBGM.js" "path": "../public/_nuxt/CED8cBGM.js"
}, },
"/_nuxt/CGzqx_GW.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"33b7-szQpwlQCIrWy2Yt6HyekQOiyQu4\"",
"mtime": "2025-11-05T12:46:41.625Z",
"size": 13239,
"path": "../public/_nuxt/CGzqx_GW.js"
},
"/_nuxt/CHQH_CwJ.js": { "/_nuxt/CHQH_CwJ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1168-mBOIwVjM5LsxIx13Cs7oT3+LJ+k\"", "etag": "\"1168-mBOIwVjM5LsxIx13Cs7oT3+LJ+k\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 4456, "size": 4456,
"path": "../public/_nuxt/CHQH_CwJ.js" "path": "../public/_nuxt/CHQH_CwJ.js"
}, },
"/_nuxt/CHkqy7mB.js": { "/_nuxt/CHkqy7mB.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"201d-h4fS2PBhZIObIxeqvtLlHIK7w00\"", "etag": "\"201d-h4fS2PBhZIObIxeqvtLlHIK7w00\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 8221, "size": 8221,
"path": "../public/_nuxt/CHkqy7mB.js" "path": "../public/_nuxt/CHkqy7mB.js"
}, },
"/_nuxt/CUq_0rkE.js": { "/_nuxt/CUq_0rkE.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"12d-JV4KW1fgT85/V3Ap13X4q2h9U3g\"", "etag": "\"12d-JV4KW1fgT85/V3Ap13X4q2h9U3g\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 301, "size": 301,
"path": "../public/_nuxt/CUq_0rkE.js" "path": "../public/_nuxt/CUq_0rkE.js"
}, },
"/_nuxt/Cb8HxufC.js": { "/_nuxt/Cb8HxufC.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"406f-TyhzTK47O3LPFk0luA/Fak0NoYQ\"", "etag": "\"406f-TyhzTK47O3LPFk0luA/Fak0NoYQ\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 16495, "size": 16495,
"path": "../public/_nuxt/Cb8HxufC.js" "path": "../public/_nuxt/Cb8HxufC.js"
}, },
"/_nuxt/CbiYaI1j.js": { "/_nuxt/CbiYaI1j.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"faa-35cv2+MngLgY4pLUjpDBDLx6vV0\"", "etag": "\"faa-35cv2+MngLgY4pLUjpDBDLx6vV0\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 4010, "size": 4010,
"path": "../public/_nuxt/CbiYaI1j.js" "path": "../public/_nuxt/CbiYaI1j.js"
}, },
"/_nuxt/CgxdOZQk.js": { "/_nuxt/CgxdOZQk.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"16e6-XCN/eSTrNul1blsGspgsYeA873w\"", "etag": "\"16e6-XCN/eSTrNul1blsGspgsYeA873w\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 5862, "size": 5862,
"path": "../public/_nuxt/CgxdOZQk.js" "path": "../public/_nuxt/CgxdOZQk.js"
}, },
"/_nuxt/Ci7R5KNW.js": { "/_nuxt/Ci7R5KNW.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"2320-2xahv071b+7opOi1O0ljQRcwIBY\"", "etag": "\"2320-2xahv071b+7opOi1O0ljQRcwIBY\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 8992, "size": 8992,
"path": "../public/_nuxt/Ci7R5KNW.js" "path": "../public/_nuxt/Ci7R5KNW.js"
}, },
"/_nuxt/CkzaQq3X.js": { "/_nuxt/CkzaQq3X.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"17d-+xKrHjeww4bpFFkkjUNLD/ebn5A\"", "etag": "\"17d-+xKrHjeww4bpFFkkjUNLD/ebn5A\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 381, "size": 381,
"path": "../public/_nuxt/CkzaQq3X.js" "path": "../public/_nuxt/CkzaQq3X.js"
}, },
"/_nuxt/CrCcIvVp.js": { "/_nuxt/CrCcIvVp.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"274-9U7hEMtgHqdnQopnKeJsBKqKyKw\"", "etag": "\"274-9U7hEMtgHqdnQopnKeJsBKqKyKw\"",
"mtime": "2025-11-05T12:46:41.625Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 628, "size": 628,
"path": "../public/_nuxt/CrCcIvVp.js" "path": "../public/_nuxt/CrCcIvVp.js"
}, },
"/_nuxt/CtbKwxql.js": { "/_nuxt/CtbKwxql.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"3090-xfpT19k0ndSinB29602tM2WBsKA\"", "etag": "\"3090-xfpT19k0ndSinB29602tM2WBsKA\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 12432, "size": 12432,
"path": "../public/_nuxt/CtbKwxql.js" "path": "../public/_nuxt/CtbKwxql.js"
}, },
"/_nuxt/CxKxaas7.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"35ed1-HTnyh8f24j/i4Ubtc6/SgRBeEaY\"",
"mtime": "2025-11-05T13:24:46.322Z",
"size": 220881,
"path": "../public/_nuxt/CxKxaas7.js"
},
"/_nuxt/DAACT36i.js": { "/_nuxt/DAACT36i.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1f9-dVOk5jAwb0VlMLJevIcT+s2NTgM\"", "etag": "\"1f9-dVOk5jAwb0VlMLJevIcT+s2NTgM\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 505, "size": 505,
"path": "../public/_nuxt/DAACT36i.js" "path": "../public/_nuxt/DAACT36i.js"
}, },
"/_nuxt/DB4lwRH4.js": { "/_nuxt/DB4lwRH4.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"285-Fn2A1Bk4lxBPdxYBguEMM5AsI0M\"", "etag": "\"285-Fn2A1Bk4lxBPdxYBguEMM5AsI0M\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 645, "size": 645,
"path": "../public/_nuxt/DB4lwRH4.js" "path": "../public/_nuxt/DB4lwRH4.js"
}, },
"/_nuxt/DEC6XYYJ.js": { "/_nuxt/DEC6XYYJ.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"37e2-qLNdHj2LfvfQ4r7oP9pxJ4234W8\"", "etag": "\"37e2-qLNdHj2LfvfQ4r7oP9pxJ4234W8\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 14306, "size": 14306,
"path": "../public/_nuxt/DEC6XYYJ.js" "path": "../public/_nuxt/DEC6XYYJ.js"
}, },
"/_nuxt/DODWL2Pr.js": { "/_nuxt/DODWL2Pr.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"e7c-ZCkB0/XZCFSnRy2p9SAs25cftgQ\"", "etag": "\"e7c-ZCkB0/XZCFSnRy2p9SAs25cftgQ\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.322Z",
"size": 3708, "size": 3708,
"path": "../public/_nuxt/DODWL2Pr.js" "path": "../public/_nuxt/DODWL2Pr.js"
}, },
"/_nuxt/DTKApGrI.js": { "/_nuxt/DTKApGrI.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1e3a-m8ONRzHj9hcIv08Kb9ZDGKcKKgE\"", "etag": "\"1e3a-m8ONRzHj9hcIv08Kb9ZDGKcKKgE\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 7738, "size": 7738,
"path": "../public/_nuxt/DTKApGrI.js" "path": "../public/_nuxt/DTKApGrI.js"
}, },
"/_nuxt/DUm-savV.js": { "/_nuxt/DUm-savV.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"378-gmau5tfuGGvvXo5HRs29CR7slTs\"", "etag": "\"378-gmau5tfuGGvvXo5HRs29CR7slTs\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 888, "size": 888,
"path": "../public/_nuxt/DUm-savV.js" "path": "../public/_nuxt/DUm-savV.js"
}, },
"/_nuxt/DaSgy0Cl.js": { "/_nuxt/DaSgy0Cl.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"11f-soKnh1qfNJj5nvt+IcgQXYvg/z4\"", "etag": "\"11f-soKnh1qfNJj5nvt+IcgQXYvg/z4\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 287, "size": 287,
"path": "../public/_nuxt/DaSgy0Cl.js" "path": "../public/_nuxt/DaSgy0Cl.js"
}, },
"/_nuxt/DaUHoOti.js": { "/_nuxt/DaUHoOti.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"177f-xSuV2n3wrsWDBysy5o9XsakrXDw\"", "etag": "\"177f-xSuV2n3wrsWDBysy5o9XsakrXDw\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 6015, "size": 6015,
"path": "../public/_nuxt/DaUHoOti.js" "path": "../public/_nuxt/DaUHoOti.js"
}, },
"/_nuxt/DdHhmCne.js": { "/_nuxt/DdHhmCne.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"22d-uKYfhsDcUsz2NrXOJmxptUGZdyE\"", "etag": "\"22d-uKYfhsDcUsz2NrXOJmxptUGZdyE\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 557, "size": 557,
"path": "../public/_nuxt/DdHhmCne.js" "path": "../public/_nuxt/DdHhmCne.js"
}, },
"/_nuxt/Dhn1q0tB.js": { "/_nuxt/Dhn1q0tB.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"280-soHXdceT8+kvIFtgcE96cO/TCIU\"", "etag": "\"280-soHXdceT8+kvIFtgcE96cO/TCIU\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 640, "size": 640,
"path": "../public/_nuxt/Dhn1q0tB.js" "path": "../public/_nuxt/Dhn1q0tB.js"
}, },
"/_nuxt/DkeYb0_S.js": { "/_nuxt/DkeYb0_S.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1ce-xiaAbRvqQ+zffTXF3Gc7rq14R0U\"", "etag": "\"1ce-xiaAbRvqQ+zffTXF3Gc7rq14R0U\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 462, "size": 462,
"path": "../public/_nuxt/DkeYb0_S.js" "path": "../public/_nuxt/DkeYb0_S.js"
}, },
"/_nuxt/DlAUqK2U.js": { "/_nuxt/DlAUqK2U.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"5b-eFCz/UrraTh721pgAl0VxBNR1es\"", "etag": "\"5b-eFCz/UrraTh721pgAl0VxBNR1es\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 91, "size": 91,
"path": "../public/_nuxt/DlAUqK2U.js" "path": "../public/_nuxt/DlAUqK2U.js"
}, },
"/_nuxt/DoQDnat2.js": { "/_nuxt/DoQDnat2.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"120d-q7H0efEMaeQo04Ily4N/PFQjhpA\"", "etag": "\"120d-q7H0efEMaeQo04Ily4N/PFQjhpA\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 4621, "size": 4621,
"path": "../public/_nuxt/DoQDnat2.js" "path": "../public/_nuxt/DoQDnat2.js"
}, },
"/_nuxt/DruLqh86.js": { "/_nuxt/DruLqh86.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"bf0-+1jS/uLKW36S7SjKCe7lwpzJbjw\"", "etag": "\"bf0-+1jS/uLKW36S7SjKCe7lwpzJbjw\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 3056, "size": 3056,
"path": "../public/_nuxt/DruLqh86.js" "path": "../public/_nuxt/DruLqh86.js"
}, },
"/_nuxt/Dy4dzDTt.js": { "/_nuxt/Dy4dzDTt.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"e61-TfoI+tUM1IJUgJ3e0hEAuF/ainI\"", "etag": "\"e61-TfoI+tUM1IJUgJ3e0hEAuF/ainI\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 3681, "size": 3681,
"path": "../public/_nuxt/Dy4dzDTt.js" "path": "../public/_nuxt/Dy4dzDTt.js"
}, },
"/_nuxt/FF_cyd6S.js": { "/_nuxt/FF_cyd6S.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"203-rairGPfunSg/yCk4txod3zRSZus\"", "etag": "\"203-rairGPfunSg/yCk4txod3zRSZus\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 515, "size": 515,
"path": "../public/_nuxt/FF_cyd6S.js" "path": "../public/_nuxt/FF_cyd6S.js"
}, },
"/_nuxt/FwQxzXsL.js": { "/_nuxt/FwQxzXsL.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"4c6b-WDmRZ/fG0kITIIN7KN6MuTnrzVc\"", "etag": "\"4c6b-WDmRZ/fG0kITIIN7KN6MuTnrzVc\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 19563, "size": 19563,
"path": "../public/_nuxt/FwQxzXsL.js" "path": "../public/_nuxt/FwQxzXsL.js"
}, },
"/_nuxt/GPPAkzQU.js": { "/_nuxt/GPPAkzQU.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"24cb-KnKpZhja4N6Fk7TQXNuaa4j4dms\"", "etag": "\"24cb-KnKpZhja4N6Fk7TQXNuaa4j4dms\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 9419, "size": 9419,
"path": "../public/_nuxt/GPPAkzQU.js" "path": "../public/_nuxt/GPPAkzQU.js"
}, },
"/_nuxt/Harheimer TC.CKfYAfp1.svg": { "/_nuxt/Harheimer TC.CKfYAfp1.svg": {
"type": "image/svg+xml", "type": "image/svg+xml",
"etag": "\"1d2535-Tx2lTuuFn2hBqGZOnDan3/OdRU0\"", "etag": "\"1d2535-Tx2lTuuFn2hBqGZOnDan3/OdRU0\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 1910069, "size": 1910069,
"path": "../public/_nuxt/Harheimer TC.CKfYAfp1.svg" "path": "../public/_nuxt/Harheimer TC.CKfYAfp1.svg"
}, },
"/_nuxt/IxY-j45w.js": { "/_nuxt/IxY-j45w.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1b54-yd8vSMFwTUtDVcZx0AViG0asyoM\"", "etag": "\"1b54-yd8vSMFwTUtDVcZx0AViG0asyoM\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 6996, "size": 6996,
"path": "../public/_nuxt/IxY-j45w.js" "path": "../public/_nuxt/IxY-j45w.js"
}, },
"/_nuxt/KxVBmS-6.js": { "/_nuxt/KxVBmS-6.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"190-F0LVEAqwB2LwyGzW0v9yzLx0v/0\"", "etag": "\"190-F0LVEAqwB2LwyGzW0v9yzLx0v/0\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 400, "size": 400,
"path": "../public/_nuxt/KxVBmS-6.js" "path": "../public/_nuxt/KxVBmS-6.js"
}, },
"/_nuxt/PsGBFO20.js": { "/_nuxt/PsGBFO20.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"34fa-VfXYDHH2Csr24waHFWok/UZ+AUQ\"", "etag": "\"34fa-VfXYDHH2Csr24waHFWok/UZ+AUQ\"",
"mtime": "2025-11-05T12:46:41.626Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 13562, "size": 13562,
"path": "../public/_nuxt/PsGBFO20.js" "path": "../public/_nuxt/PsGBFO20.js"
}, },
"/_nuxt/Qy3ajxTk.js": { "/_nuxt/Qy3ajxTk.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"13f-LgrH17St2xFg+RPGvT3uJRaRfFw\"", "etag": "\"13f-LgrH17St2xFg+RPGvT3uJRaRfFw\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 319, "size": 319,
"path": "../public/_nuxt/Qy3ajxTk.js" "path": "../public/_nuxt/Qy3ajxTk.js"
}, },
"/_nuxt/R6Iy1jPP.js": { "/_nuxt/R6Iy1jPP.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"137-QCUizOitouzMVC2drCYFTAZmqPU\"", "etag": "\"137-QCUizOitouzMVC2drCYFTAZmqPU\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 311, "size": 311,
"path": "../public/_nuxt/R6Iy1jPP.js" "path": "../public/_nuxt/R6Iy1jPP.js"
}, },
"/_nuxt/YJHbYJtA.js": { "/_nuxt/YJHbYJtA.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"19f-nQw578pUen9o8yYaMA8Bwag6xho\"", "etag": "\"19f-nQw578pUen9o8yYaMA8Bwag6xho\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 415, "size": 415,
"path": "../public/_nuxt/YJHbYJtA.js" "path": "../public/_nuxt/YJHbYJtA.js"
}, },
"/_nuxt/ZrOCUSmD.js": { "/_nuxt/ZrOCUSmD.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"ee2-knvq//8tl4tcmRjFy6nWAy0dRBk\"", "etag": "\"ee2-knvq//8tl4tcmRjFy6nWAy0dRBk\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 3810, "size": 3810,
"path": "../public/_nuxt/ZrOCUSmD.js" "path": "../public/_nuxt/ZrOCUSmD.js"
}, },
"/_nuxt/entry.CYvTzE5H.css": { "/_nuxt/entry.wEUbgGlA.css": {
"type": "text/css; charset=utf-8", "type": "text/css; charset=utf-8",
"etag": "\"d359-K8d7YngVKCHFgXk5rvr7JvAWgRM\"", "etag": "\"d470-jKdybOkNd/weF4HvmA1i+mSz4pw\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 54105, "size": 54384,
"path": "../public/_nuxt/entry.CYvTzE5H.css" "path": "../public/_nuxt/entry.wEUbgGlA.css"
}, },
"/_nuxt/error-404.CbXQcqJW.css": { "/_nuxt/error-404.CbXQcqJW.css": {
"type": "text/css; charset=utf-8", "type": "text/css; charset=utf-8",
"etag": "\"97e-Ty5bTTSEudJkO/DsGUoIf37xYxc\"", "etag": "\"97e-Ty5bTTSEudJkO/DsGUoIf37xYxc\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 2430, "size": 2430,
"path": "../public/_nuxt/error-404.CbXQcqJW.css" "path": "../public/_nuxt/error-404.CbXQcqJW.css"
}, },
"/_nuxt/error-500.L485xXhD.css": { "/_nuxt/error-500.L485xXhD.css": {
"type": "text/css; charset=utf-8", "type": "text/css; charset=utf-8",
"etag": "\"773-jNt1QdCa+iqaSZb1mv/IQWC5p6w\"", "etag": "\"773-jNt1QdCa+iqaSZb1mv/IQWC5p6w\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 1907, "size": 1907,
"path": "../public/_nuxt/error-500.L485xXhD.css" "path": "../public/_nuxt/error-500.L485xXhD.css"
}, },
"/_nuxt/index.BfDdQit3.css": { "/_nuxt/index.BfDdQit3.css": {
"type": "text/css; charset=utf-8", "type": "text/css; charset=utf-8",
"etag": "\"1db-8CoCw55otGXQVF3jz4vWPcGWmuY\"", "etag": "\"1db-8CoCw55otGXQVF3jz4vWPcGWmuY\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 475, "size": 475,
"path": "../public/_nuxt/index.BfDdQit3.css" "path": "../public/_nuxt/index.BfDdQit3.css"
}, },
"/_nuxt/oL_Xi1h-.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"35cb0-7x109VLkapMthU3oJP0Ty7wYQpk\"",
"mtime": "2025-11-05T12:46:41.627Z",
"size": 220336,
"path": "../public/_nuxt/oL_Xi1h-.js"
},
"/_nuxt/oN0_bS6A.js": { "/_nuxt/oN0_bS6A.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1d3-DKhiaT2RUlSXk55jBttctUuTQQI\"", "etag": "\"1d3-DKhiaT2RUlSXk55jBttctUuTQQI\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.323Z",
"size": 467, "size": 467,
"path": "../public/_nuxt/oN0_bS6A.js" "path": "../public/_nuxt/oN0_bS6A.js"
}, },
"/_nuxt/r4K7wPft.js": {
"type": "text/javascript; charset=utf-8",
"etag": "\"4c51-7zSVu+BLyGsMT1uGYy/MrmouUkg\"",
"mtime": "2025-11-05T13:24:46.323Z",
"size": 19537,
"path": "../public/_nuxt/r4K7wPft.js"
},
"/_nuxt/rVkivqmM.js": { "/_nuxt/rVkivqmM.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"161a-zK8c8X4CzA56nT/n6kqKbIto9uw\"", "etag": "\"161a-zK8c8X4CzA56nT/n6kqKbIto9uw\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.324Z",
"size": 5658, "size": 5658,
"path": "../public/_nuxt/rVkivqmM.js" "path": "../public/_nuxt/rVkivqmM.js"
}, },
"/_nuxt/t1jwZBF5.js": { "/_nuxt/t1jwZBF5.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"1dc5-WYQUOK4isA/E8pj6xugVdZaBBKo\"", "etag": "\"1dc5-WYQUOK4isA/E8pj6xugVdZaBBKo\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.324Z",
"size": 7621, "size": 7621,
"path": "../public/_nuxt/t1jwZBF5.js" "path": "../public/_nuxt/t1jwZBF5.js"
}, },
"/_nuxt/t6YtlcxA.js": { "/_nuxt/t6YtlcxA.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"575-I2e3m9LKSy82cY3tyiVI/I7zXz4\"", "etag": "\"575-I2e3m9LKSy82cY3tyiVI/I7zXz4\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.324Z",
"size": 1397, "size": 1397,
"path": "../public/_nuxt/t6YtlcxA.js" "path": "../public/_nuxt/t6YtlcxA.js"
}, },
"/_nuxt/tgU1A5jl.js": { "/_nuxt/tgU1A5jl.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"576-TwB1Mj1tLB6Z9zhK9LTHVXbtW8k\"", "etag": "\"576-TwB1Mj1tLB6Z9zhK9LTHVXbtW8k\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.324Z",
"size": 1398, "size": 1398,
"path": "../public/_nuxt/tgU1A5jl.js" "path": "../public/_nuxt/tgU1A5jl.js"
}, },
"/_nuxt/vygoqABK.js": { "/_nuxt/vygoqABK.js": {
"type": "text/javascript; charset=utf-8", "type": "text/javascript; charset=utf-8",
"etag": "\"28f-AjrUgCN/auxu+4F13IlxhdOmeFk\"", "etag": "\"28f-AjrUgCN/auxu+4F13IlxhdOmeFk\"",
"mtime": "2025-11-05T12:46:41.627Z", "mtime": "2025-11-05T13:24:46.324Z",
"size": 655, "size": 655,
"path": "../public/_nuxt/vygoqABK.js" "path": "../public/_nuxt/vygoqABK.js"
}, },
"/spielplaene/1. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"64c6-+477M+gD/spwpWR9NO/tMJ/inCc\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 25798,
"path": "../public/spielplaene/1. Mannschaft 20252026.pdf"
},
"/spielplaene/2. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"5bfa-DRJMHLV15iss67lEISoGqSYmZjE\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 23546,
"path": "../public/spielplaene/2. Mannschaft 20252026.pdf"
},
"/spielplaene/3. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"7447-w933CPQdXhkWJ2AZOVdY0UgJnPo\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 29767,
"path": "../public/spielplaene/3. Mannschaft 20252026.pdf"
},
"/spielplaene/4. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"6a9b-4TPGn1yQlFUMRj7oB43SN//Np9o\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 27291,
"path": "../public/spielplaene/4. Mannschaft 20252026.pdf"
},
"/spielplaene/5. Mannschaft 20252026.pdf": {
"type": "application/pdf",
"etag": "\"6523-5VUfCMaoiNhcwHhptHHTVJ3lSwQ\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 25891,
"path": "../public/spielplaene/5. Mannschaft 20252026.pdf"
},
"/spielplaene/Jugend 11 20252026.pdf": {
"type": "application/pdf",
"etag": "\"52e9-3Rrk9UKUxPh80pBJ0w9oLVbe5dA\"",
"mtime": "2025-11-05T13:24:46.326Z",
"size": 21225,
"path": "../public/spielplaene/Jugend 11 20252026.pdf"
},
"/documents/spielplaene/README.md": { "/documents/spielplaene/README.md": {
"type": "text/markdown; charset=utf-8", "type": "text/markdown; charset=utf-8",
"etag": "\"229-MzuxTQlQrWciSShfjF7Fr2QGclE\"", "etag": "\"229-MzuxTQlQrWciSShfjF7Fr2QGclE\"",
"mtime": "2025-11-05T12:46:41.629Z", "mtime": "2025-11-05T13:24:46.326Z",
"size": 553, "size": 553,
"path": "../public/documents/spielplaene/README.md" "path": "../public/documents/spielplaene/README.md"
}, },
"/_nuxt/builds/latest.json": { "/_nuxt/builds/latest.json": {
"type": "application/json", "type": "application/json",
"etag": "\"47-DSEOTikTqgipcQKtRl6C4eJX/eM\"", "etag": "\"47-VzWK2iAeXt6NcwoFjDqObFk+lQ4\"",
"mtime": "2025-11-05T12:46:41.615Z", "mtime": "2025-11-05T13:24:46.310Z",
"size": 71, "size": 71,
"path": "../public/_nuxt/builds/latest.json" "path": "../public/_nuxt/builds/latest.json"
}, },
"/_nuxt/builds/meta/9438b35d-0d10-4203-a329-f7a1e287e2a1.json": { "/_nuxt/builds/meta/8af76f9e-6e85-416c-9e2f-92c68e0dfd76.json": {
"type": "application/json", "type": "application/json",
"etag": "\"8b-J28GxNhENWmuwxbIXkR3zmEQygk\"", "etag": "\"8b-cx0wXpS3b8fz/gEJyMvOnottRyQ\"",
"mtime": "2025-11-05T12:46:41.613Z", "mtime": "2025-11-05T13:24:46.308Z",
"size": 139, "size": 139,
"path": "../public/_nuxt/builds/meta/9438b35d-0d10-4203-a329-f7a1e287e2a1.json" "path": "../public/_nuxt/builds/meta/8af76f9e-6e85-416c-9e2f-92c68e0dfd76.json"
} }
}; };
@@ -5652,6 +5659,7 @@ const _lazy_EK_x5_ = () => import('../routes/api/galerie.get.mjs');
const _lazy_jYLuY1 = () => import('../routes/api/members.delete.mjs'); const _lazy_jYLuY1 = () => import('../routes/api/members.delete.mjs');
const _lazy_HGAbG3 = () => import('../routes/api/members.get.mjs'); const _lazy_HGAbG3 = () => import('../routes/api/members.get.mjs');
const _lazy_XNetVh = () => import('../routes/api/members.post.mjs'); const _lazy_XNetVh = () => import('../routes/api/members.post.mjs');
const _lazy_HlxJ2p = () => import('../routes/api/members/bulk.post.mjs');
const _lazy_AgsX_N = () => import('../routes/api/membership/applications.get.mjs'); const _lazy_AgsX_N = () => import('../routes/api/membership/applications.get.mjs');
const _lazy_7df2qh = () => import('../routes/api/membership/download/_id_.get.mjs'); const _lazy_7df2qh = () => import('../routes/api/membership/download/_id_.get.mjs');
const _lazy_Od0rSJ = () => import('../routes/api/membership/generate-pdf.post.mjs'); const _lazy_Od0rSJ = () => import('../routes/api/membership/generate-pdf.post.mjs');
@@ -5694,6 +5702,7 @@ const handlers = [
{ route: '/api/members', handler: _lazy_jYLuY1, lazy: true, middleware: false, method: "delete" }, { route: '/api/members', handler: _lazy_jYLuY1, lazy: true, middleware: false, method: "delete" },
{ route: '/api/members', handler: _lazy_HGAbG3, lazy: true, middleware: false, method: "get" }, { route: '/api/members', handler: _lazy_HGAbG3, lazy: true, middleware: false, method: "get" },
{ route: '/api/members', handler: _lazy_XNetVh, lazy: true, middleware: false, method: "post" }, { route: '/api/members', handler: _lazy_XNetVh, lazy: true, middleware: false, method: "post" },
{ route: '/api/members/bulk', handler: _lazy_HlxJ2p, lazy: true, middleware: false, method: "post" },
{ route: '/api/membership/applications', handler: _lazy_AgsX_N, lazy: true, middleware: false, method: "get" }, { route: '/api/membership/applications', handler: _lazy_AgsX_N, lazy: true, middleware: false, method: "get" },
{ route: '/api/membership/download/:id', handler: _lazy_7df2qh, lazy: true, middleware: false, method: "get" }, { route: '/api/membership/download/:id', handler: _lazy_7df2qh, lazy: true, middleware: false, method: "get" },
{ route: '/api/membership/generate-pdf', handler: _lazy_Od0rSJ, lazy: true, middleware: false, method: "post" }, { route: '/api/membership/generate-pdf', handler: _lazy_Od0rSJ, lazy: true, middleware: false, method: "post" },

View File

@@ -61,6 +61,8 @@ const login_post = defineEventHandler(async (event) => {
}); });
return { return {
success: true, success: true,
token,
// Token auch im Body für externe API-Clients
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,

View File

@@ -1 +1 @@
{"version":3,"file":"login.post.mjs","sources":["../../../../../../server/api/auth/login.post.js"],"sourcesContent":null,"names":[],"mappings":";;;;;;;;;;;;;;;;;AAEA,mBAAA,kBAAA,CAAA,OAAA,KAAA,KAAA;AACA,EAAA,IAAA;AACA,IAAA,MAAA,IAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA;AACA,IAAA,MAAA,EAAA,KAAA,EAAA,QAAA,EAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,KAAA,IAAA,CAAA,QAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,MAAA,SAAA,EAAA;AACA,IAAA,MAAA,IAAA,GAAA,KAAA,CAAA,IAAA,CAAA,CAAA,CAAA,KAAA,CAAA,CAAA,MAAA,WAAA,EAAA,KAAA,KAAA,CAAA,WAAA,EAAA,CAAA;AAEA,IAAA,IAAA,CAAA,IAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,IAAA,IAAA,CAAA,WAAA,KAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,OAAA,GAAA,MAAA,cAAA,CAAA,QAAA,EAAA,KAAA,QAAA,CAAA;AACA,IAAA,IAAA,CAAA,OAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,cAAA,IAAA,CAAA;AAGA,IAAA,MAAA,aAAA,CAAA,IAAA,CAAA,EAAA,EAAA,KAAA,CAAA;AAGA,IAAA,IAAA,CAAA,SAAA,GAAA,iBAAA,IAAA,IAAA,EAAA,EAAA,WAAA,EAAA;AACA,IAAA,MAAA,YAAA,GAAA,MAAA,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,EAAA,KAAA,IAAA,CAAA,EAAA,GAAA,IAAA,GAAA,CAAA,CAAA;AACA,IAAA,MAAA,WAAA,YAAA,CAAA;AAGA,IAAA,SAAA,CAAA,KAAA,EAAA,cAAA,KAAA,EAAA;AAAA,MACA,QAAA,EAAA,IAAA;AAAA,MACA,MAAA,EAAA,KAAA;AAAA;AAAA,MACA,QAAA,EAAA,KAAA;AAAA,MACA,MAAA,EAAA,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA;AAAA;AAAA,KACA,CAAA;AAGA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,IAAA,EAAA;AAAA,QACA,IAAA,IAAA,CAAA,EAAA;AAAA,QACA,OAAA,IAAA,CAAA,KAAA;AAAA,QACA,MAAA,IAAA,CAAA,IAAA;AAAA,QACA,MAAA,IAAA,CAAA;AAAA;AACA,KACA;AAAA,EACA,SAAA,KAAA,EAAA;AACA,IAAA,OAAA,CAAA,KAAA,CAAA,iBAAA,KAAA,CAAA;AACA,IAAA,MAAA,KAAA;AAAA,EACA;AACA,CAAA,CAAA;;;;"} {"version":3,"file":"login.post.mjs","sources":["../../../../../../server/api/auth/login.post.js"],"sourcesContent":null,"names":[],"mappings":";;;;;;;;;;;;;;;;;AAEA,mBAAA,kBAAA,CAAA,OAAA,KAAA,KAAA;AACA,EAAA,IAAA;AACA,IAAA,MAAA,IAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA;AACA,IAAA,MAAA,EAAA,KAAA,EAAA,QAAA,EAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,KAAA,IAAA,CAAA,QAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,MAAA,SAAA,EAAA;AACA,IAAA,MAAA,IAAA,GAAA,KAAA,CAAA,IAAA,CAAA,CAAA,CAAA,KAAA,CAAA,CAAA,MAAA,WAAA,EAAA,KAAA,KAAA,CAAA,WAAA,EAAA,CAAA;AAEA,IAAA,IAAA,CAAA,IAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,IAAA,IAAA,CAAA,WAAA,KAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,OAAA,GAAA,MAAA,cAAA,CAAA,QAAA,EAAA,KAAA,QAAA,CAAA;AACA,IAAA,IAAA,CAAA,OAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAGA,IAAA,MAAA,KAAA,GAAA,cAAA,IAAA,CAAA;AAGA,IAAA,MAAA,aAAA,CAAA,IAAA,CAAA,EAAA,EAAA,KAAA,CAAA;AAGA,IAAA,IAAA,CAAA,SAAA,GAAA,iBAAA,IAAA,IAAA,EAAA,EAAA,WAAA,EAAA;AACA,IAAA,MAAA,YAAA,GAAA,MAAA,GAAA,CAAA,CAAA,CAAA,KAAA,EAAA,EAAA,KAAA,IAAA,CAAA,EAAA,GAAA,IAAA,GAAA,CAAA,CAAA;AACA,IAAA,MAAA,WAAA,YAAA,CAAA;AAGA,IAAA,SAAA,CAAA,KAAA,EAAA,cAAA,KAAA,EAAA;AAAA,MACA,QAAA,EAAA,IAAA;AAAA,MACA,MAAA,EAAA,KAAA;AAAA;AAAA,MACA,QAAA,EAAA,KAAA;AAAA,MACA,MAAA,EAAA,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA;AAAA;AAAA,KACA,CAAA;AAGA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,KAAA;AAAA;AAAA,MACA,IAAA,EAAA;AAAA,QACA,IAAA,IAAA,CAAA,EAAA;AAAA,QACA,OAAA,IAAA,CAAA,KAAA;AAAA,QACA,MAAA,IAAA,CAAA,IAAA;AAAA,QACA,MAAA,IAAA,CAAA;AAAA;AACA,KACA;AAAA,EACA,SAAA,KAAA,EAAA;AACA,IAAA,OAAA,CAAA,KAAA,CAAA,iBAAA,KAAA,CAAA;AACA,IAAA,MAAA,KAAA;AAAA,EACA;AACA,CAAA,CAAA;;;;"}

View File

@@ -1,4 +1,4 @@
import { d as defineEventHandler, g as getCookie, c as createError, r as readBody } from '../../nitro/nitro.mjs'; import { d as defineEventHandler, g as getCookie, b as getHeader, c as createError, r as readBody } from '../../nitro/nitro.mjs';
import { b as verifyToken, e as getUserById } from '../../_/auth.mjs'; import { b as verifyToken, e as getUserById } from '../../_/auth.mjs';
import { s as saveMember } from '../../_/members.mjs'; import { s as saveMember } from '../../_/members.mjs';
import 'node:http'; import 'node:http';
@@ -18,11 +18,17 @@ import 'crypto';
const members_post = defineEventHandler(async (event) => { const members_post = defineEventHandler(async (event) => {
try { try {
const token = getCookie(event, "auth_token"); let token = getCookie(event, "auth_token");
if (!token) {
const authHeader = getHeader(event, "authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
}
if (!token) { if (!token) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
message: "Nicht authentifiziert." message: "Nicht authentifiziert. Bitte Token im Cookie oder Authorization-Header bereitstellen."
}); });
} }
const decoded = verifyToken(token); const decoded = verifyToken(token);
@@ -33,36 +39,65 @@ const members_post = defineEventHandler(async (event) => {
}); });
} }
const user = await getUserById(decoded.id); const user = await getUserById(decoded.id);
if (!user || user.role !== "admin" && user.role !== "vorstand") { if (!user) {
throw createError({
statusCode: 401,
message: "Benutzer nicht gefunden."
});
}
if (user.role !== "admin" && user.role !== "vorstand") {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
message: "Keine Berechtigung zum Bearbeiten von Mitgliedern." message: "Keine Berechtigung zum Hinzuf\xFCgen/Bearbeiten von Mitgliedern. Erforderlich: admin oder vorstand Rolle."
}); });
} }
const body = await readBody(event); const body = await readBody(event);
const { id, firstName, lastName, email, phone, address, notes } = body; const { id, firstName, lastName, geburtsdatum, email, phone, address, notes } = body;
if (!firstName || !lastName) { if (!firstName || !lastName) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
message: "Vorname und Nachname sind erforderlich." message: "Vorname und Nachname sind erforderlich."
}); });
} }
await saveMember({ if (!geburtsdatum) {
id: id || void 0, throw createError({
firstName, statusCode: 400,
lastName, message: "Geburtsdatum ist erforderlich, um Duplikate zu vermeiden."
email: email || "", });
phone: phone || "", }
address: address || "", try {
notes: notes || "" await saveMember({
}); id: id || void 0,
return { firstName,
success: true, lastName,
message: "Mitglied erfolgreich gespeichert." geburtsdatum: geburtsdatum || "",
}; email: email || "",
phone: phone || "",
address: address || "",
notes: notes || ""
});
return {
success: true,
message: "Mitglied erfolgreich gespeichert."
};
} catch (memberError) {
if (memberError.message && memberError.message.includes("existiert bereits")) {
throw createError({
statusCode: 409,
message: memberError.message
});
}
throw memberError;
}
} catch (error) { } catch (error) {
console.error("Fehler beim Speichern des Mitglieds:", error); console.error("Fehler beim Speichern des Mitglieds:", error);
throw error; if (error.statusCode) {
throw error;
}
throw createError({
statusCode: error.statusCode || 500,
message: error.message || "Fehler beim Speichern des Mitglieds."
});
} }
}); });

View File

@@ -1 +1 @@
{"version":3,"file":"members.post.mjs","sources":["../../../../../server/api/members.post.js"],"sourcesContent":null,"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAGA,qBAAA,kBAAA,CAAA,OAAA,KAAA,KAAA;AACA,EAAA,IAAA;AACA,IAAA,MAAA,KAAA,GAAA,SAAA,CAAA,KAAA,EAAA,YAAA,CAAA;AAEA,IAAA,IAAA,CAAA,KAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAEA,IAAA,MAAA,OAAA,GAAA,YAAA,KAAA,CAAA;AAEA,IAAA,IAAA,CAAA,OAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAEA,IAAA,MAAA,IAAA,GAAA,MAAA,WAAA,CAAA,OAAA,CAAA,EAAA,CAAA;AAGA,IAAA,IAAA,CAAA,IAAA,IAAA,IAAA,CAAA,SAAA,OAAA,IAAA,IAAA,CAAA,SAAA,UAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAEA,IAAA,MAAA,IAAA,GAAA,MAAA,QAAA,CAAA,KAAA,CAAA;AACA,IAAA,MAAA,EAAA,IAAA,SAAA,EAAA,QAAA,EAAA,OAAA,KAAA,EAAA,OAAA,EAAA,OAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,SAAA,IAAA,CAAA,QAAA,EAAA;AACA,MAAA,MAAA,WAAA,CAAA;AAAA,QACA,UAAA,EAAA,GAAA;AAAA,QACA,OAAA,EAAA;AAAA,OACA,CAAA;AAAA,IACA;AAEA,IAAA,MAAA,UAAA,CAAA;AAAA,MACA,IAAA,EAAA,IAAA,KAAA,CAAA;AAAA,MACA,SAAA;AAAA,MACA,QAAA;AAAA,MACA,OAAA,KAAA,IAAA,EAAA;AAAA,MACA,OAAA,KAAA,IAAA,EAAA;AAAA,MACA,SAAA,OAAA,IAAA,EAAA;AAAA,MACA,OAAA,KAAA,IAAA;AAAA,KACA,CAAA;AAEA,IAAA,OAAA;AAAA,MACA,OAAA,EAAA,IAAA;AAAA,MACA,OAAA,EAAA;AAAA,KACA;AAAA,EACA,SAAA,KAAA,EAAA;AACA,IAAA,OAAA,CAAA,KAAA,CAAA,wCAAA,KAAA,CAAA;AACA,IAAA,MAAA,KAAA;AAAA,EACA;AACA,CAAA,CAAA;;;;"} {"version":3,"file":"members.post.mjs","sources":["../../../../../server/api/members.post.js"],"sourcesContent":null,"names":[],"mappings":"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"}

View File

@@ -272,7 +272,7 @@ async function renderInlineStyles(usedModules) {
const renderSSRHeadOptions = {"omitLineBreaks":true}; const renderSSRHeadOptions = {"omitLineBreaks":true};
const entryFileName = "oL_Xi1h-.js"; const entryFileName = "CxKxaas7.js";
globalThis.__buildAssetsURL = buildAssetsURL; globalThis.__buildAssetsURL = buildAssetsURL;
globalThis.__publicAssetsURL = publicAssetsURL; globalThis.__publicAssetsURL = publicAssetsURL;

View File

@@ -5,18 +5,14 @@
<div class="flex flex-col justify-between h-full py-2"> <div class="flex flex-col justify-between h-full py-2">
<!-- Hauptmenü --> <!-- Hauptmenü -->
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<!-- Logo --> <!-- Logo -->
<NuxtLink to="/" class="flex items-center space-x-3 hover:scale-105 transition-transform"> <NuxtLink to="/" class="flex items-center space-x-3 hover:scale-105 transition-transform">
<img <img src="~/assets/images/logos/Harheimer TC.svg" alt="Harheimer TC Logo" class="w-12 h-12" />
src="~/assets/images/logos/Harheimer TC.svg" <div class="hidden sm:block">
alt="Harheimer TC Logo" <span class="text-xl font-display font-bold text-white">Harheimer <span
class="w-12 h-12" class="text-primary-400">TC</span></span>
/> </div>
<div class="hidden sm:block"> </NuxtLink>
<span class="text-xl font-display font-bold text-white">Harheimer <span
class="text-primary-400">TC</span></span>
</div>
</NuxtLink>
<div style="display:flex;flex-direction:column;"> <div style="display:flex;flex-direction:column;">
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
@@ -57,18 +53,13 @@
Termine Termine
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink v-if="hasGalleryImages" to="/galerie" @click="currentSubmenu = null"
v-if="hasGalleryImages"
to="/galerie"
@click="currentSubmenu = null"
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50" class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
active-class="text-white bg-primary-600"> active-class="text-white bg-primary-600">
Galerie Galerie
</NuxtLink> </NuxtLink>
<button <button v-if="isLoggedIn" @click="toggleSubmenu('intern')"
v-if="isLoggedIn"
@click="toggleSubmenu('intern')"
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50" class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
:class="(route.path.startsWith('/mitgliederbereich') || route.path.startsWith('/cms') || currentSubmenu === 'intern') ? 'text-white bg-primary-600' : ''"> :class="(route.path.startsWith('/mitgliederbereich') || route.path.startsWith('/cms') || currentSubmenu === 'intern') ? 'text-white bg-primary-600' : ''">
Intern Intern
@@ -81,203 +72,192 @@
</div> </div>
<div class="hidden lg:flex items-center h-6 border-t border-primary-700/20"> <div class="hidden lg:flex items-center h-6 border-t border-primary-700/20">
<div v-if="currentSubmenu" class="flex items-center space-x-1"> <div v-if="currentSubmenu" class="flex items-center space-x-1">
<!-- Verein Submenu --> <!-- Verein Submenu -->
<template v-if="currentSubmenu === 'verein'"> <template v-if="currentSubmenu === 'verein'">
<NuxtLink to="/verein/ueber-uns" <NuxtLink to="/verein/ueber-uns"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Über uns
</NuxtLink>
<NuxtLink to="/vorstand"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Vorstand
</NuxtLink>
<NuxtLink to="/verein/geschichte"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Geschichte
</NuxtLink>
<NuxtLink to="/verein/satzung"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Satzung
</NuxtLink>
<NuxtLink to="/vereinsmeisterschaften"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Vereinsmeisterschaften
</NuxtLink>
</template>
<!-- Mannschaften Submenu -->
<template v-if="currentSubmenu === 'mannschaften'">
<NuxtLink to="/mannschaften"
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="bg-primary-600">
Übersicht
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<template v-for="mannschaft in mannschaften" :key="mannschaft.slug">
<NuxtLink
:to="`/mannschaften/${mannschaft.slug}`"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"> active-class="text-white bg-primary-600">
{{ mannschaft.mannschaft }} Über uns
</NuxtLink>
<NuxtLink to="/vorstand"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Vorstand
</NuxtLink>
<NuxtLink to="/verein/geschichte"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Geschichte
</NuxtLink>
<NuxtLink to="/verein/satzung"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Satzung
</NuxtLink>
<NuxtLink to="/vereinsmeisterschaften"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Vereinsmeisterschaften
</NuxtLink> </NuxtLink>
</template> </template>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink to="/mannschaften/spielplaene"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Spielpläne
</NuxtLink>
<NuxtLink to="/spielsysteme"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Spielsysteme
</NuxtLink>
</template>
<!-- Training Submenu --> <!-- Mannschaften Submenu -->
<template v-if="currentSubmenu === 'training'"> <template v-if="currentSubmenu === 'mannschaften'">
<NuxtLink to="/training" <NuxtLink to="/mannschaften"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all" class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600"> active-class="bg-primary-600">
Trainingszeiten Übersicht
</NuxtLink> </NuxtLink>
<NuxtLink to="/training/trainer"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Trainer
</NuxtLink>
<NuxtLink to="/training/anfaenger"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Anfänger
</NuxtLink>
<NuxtLink to="/tt-regeln"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
TT-Regeln
</NuxtLink>
</template>
<!-- Intern Submenu -->
<template v-if="currentSubmenu === 'intern'">
<NuxtLink to="/mitgliederbereich"
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="bg-primary-600">
Übersicht
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink to="/mitgliederbereich/mitglieder"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Mitgliederliste
</NuxtLink>
<NuxtLink to="/mitgliederbereich/news"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
News
</NuxtLink>
<NuxtLink to="/mitgliederbereich/profil"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Mein Profil
</NuxtLink>
<template v-if="isAdmin">
<div class="h-3 w-px bg-primary-700" /> <div class="h-3 w-px bg-primary-700" />
<div class="relative inline-block"> <template v-for="mannschaft in mannschaften" :key="mannschaft.slug">
<button <NuxtLink :to="`/mannschaften/${mannschaft.slug}`"
@click.stop="toggleCmsDropdown" class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
class="px-2.5 py-1 text-xs text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded transition-all flex items-center" active-class="text-white bg-primary-600">
:class="route.path.startsWith('/cms') ? 'text-white bg-primary-600' : ''" {{ mannschaft.mannschaft }}
> </NuxtLink>
CMS </template>
<ChevronDown :size="12" class="ml-1" :class="['transition-transform', showCmsDropdown ? 'rotate-180' : '']" /> <div class="h-3 w-px bg-primary-700" />
</button> <NuxtLink to="/mannschaften/spielplaene"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
<!-- CMS Dropdown --> active-class="text-white bg-primary-600">
<div Spielpläne
v-if="showCmsDropdown" </NuxtLink>
class="absolute left-0 top-full mt-1 w-48 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden z-50" <NuxtLink to="/spielsysteme"
> class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
<NuxtLink to="/cms" active-class="text-white bg-primary-600">
@click="showCmsDropdown = false" Spielsysteme
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors"> </NuxtLink>
Übersicht
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/cms/ueber-uns"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Über uns
</NuxtLink>
<NuxtLink to="/cms/geschichte"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Geschichte
</NuxtLink>
<NuxtLink to="/cms/tt-regeln"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
TT-Regeln
</NuxtLink>
<NuxtLink to="/cms/satzung"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Satzung
</NuxtLink>
<NuxtLink to="/cms/vereinsmeisterschaften"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Vereinsmeisterschaften
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/mitgliederbereich/news"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
News
</NuxtLink>
<NuxtLink to="/cms/termine"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Termine
</NuxtLink>
<NuxtLink to="/cms/spielplaene"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Spielpläne
</NuxtLink>
<NuxtLink to="/mitgliederbereich/mitglieder"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Mitglieder
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/cms/einstellungen"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Einstellungen
</NuxtLink>
<NuxtLink to="/cms/mitgliedschaftsantraege"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Mitgliedschaftsanträge
</NuxtLink>
<NuxtLink to="/cms/benutzer"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Benutzerverwaltung
</NuxtLink>
</div>
</div>
</template> </template>
</template>
<!-- Training Submenu -->
<template v-if="currentSubmenu === 'training'">
<NuxtLink to="/training"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Trainingszeiten
</NuxtLink>
<NuxtLink to="/training/trainer"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Trainer
</NuxtLink>
<NuxtLink to="/training/anfaenger"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Anfänger
</NuxtLink>
<NuxtLink to="/tt-regeln"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
TT-Regeln
</NuxtLink>
</template>
<!-- Intern Submenu -->
<template v-if="currentSubmenu === 'intern'">
<NuxtLink to="/mitgliederbereich"
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
active-class="bg-primary-600">
Übersicht
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink to="/mitgliederbereich/mitglieder"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Mitgliederliste
</NuxtLink>
<NuxtLink to="/mitgliederbereich/news"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
News
</NuxtLink>
<NuxtLink to="/mitgliederbereich/profil"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
Mein Profil
</NuxtLink>
<div class="h-3 w-px bg-primary-700" />
<NuxtLink to="/mitgliederbereich/api"
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
active-class="text-white bg-primary-600">
API-Dokumentation
</NuxtLink>
<template v-if="isAdmin">
<div class="h-3 w-px bg-primary-700" />
<div class="relative inline-block">
<button @click.stop="toggleCmsDropdown"
class="px-2.5 py-1 text-xs text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded transition-all flex items-center"
:class="route.path.startsWith('/cms') ? 'text-white bg-primary-600' : ''">
CMS
<ChevronDown :size="12" class="ml-1"
:class="['transition-transform', showCmsDropdown ? 'rotate-180' : '']" />
</button>
<!-- CMS Dropdown -->
<div v-if="showCmsDropdown"
class="absolute left-0 top-full mt-1 w-48 bg-gray-800 border border-gray-700 rounded-lg shadow-xl overflow-hidden z-50">
<NuxtLink to="/cms" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Übersicht
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/cms/ueber-uns" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Über uns
</NuxtLink>
<NuxtLink to="/cms/geschichte" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Geschichte
</NuxtLink>
<NuxtLink to="/cms/tt-regeln" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
TT-Regeln
</NuxtLink>
<NuxtLink to="/cms/satzung" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Satzung
</NuxtLink>
<NuxtLink to="/cms/vereinsmeisterschaften" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Vereinsmeisterschaften
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/mitgliederbereich/news" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
News
</NuxtLink>
<NuxtLink to="/cms/termine" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Termine
</NuxtLink>
<NuxtLink to="/cms/spielplaene" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Spielpläne
</NuxtLink>
<NuxtLink to="/mitgliederbereich/mitglieder" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Mitglieder
</NuxtLink>
<div class="border-t border-gray-700 my-1"></div>
<NuxtLink to="/cms/einstellungen" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Einstellungen
</NuxtLink>
<NuxtLink to="/cms/mitgliedschaftsantraege" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Mitgliedschaftsanträge
</NuxtLink>
<NuxtLink to="/cms/benutzer" @click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Benutzerverwaltung
</NuxtLink>
</div>
</div>
</template>
</template>
</div>
</div> </div>
</div>
<!-- Mobile Menu Button --> <!-- Mobile Menu Button -->
<button @click="isMobileMenuOpen = !isMobileMenuOpen" <button @click="isMobileMenuOpen = !isMobileMenuOpen"
@@ -326,16 +306,16 @@
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Geschichte Geschichte
</NuxtLink> </NuxtLink>
<NuxtLink to="/verein/satzung" @click="isMobileMenuOpen = false" <NuxtLink to="/verein/satzung" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Satzung Satzung
</NuxtLink> </NuxtLink>
<NuxtLink to="/vereinsmeisterschaften" @click="isMobileMenuOpen = false" <NuxtLink to="/vereinsmeisterschaften" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Vereinsmeisterschaften Vereinsmeisterschaften
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
<!-- Mannschaften Mobile --> <!-- Mannschaften Mobile -->
<div> <div>
@@ -351,24 +331,22 @@
Übersicht Übersicht
</NuxtLink> </NuxtLink>
<template v-for="mannschaft in mannschaften" :key="mannschaft.slug"> <template v-for="mannschaft in mannschaften" :key="mannschaft.slug">
<NuxtLink <NuxtLink :to="`/mannschaften/${mannschaft.slug}`" @click="isMobileMenuOpen = false"
:to="`/mannschaften/${mannschaft.slug}`"
@click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
{{ mannschaft.mannschaft }} {{ mannschaft.mannschaft }}
</NuxtLink> </NuxtLink>
</template> </template>
<div class="border-t border-primary-700/20 my-2" /> <div class="border-t border-primary-700/20 my-2" />
<NuxtLink to="/mannschaften/spielplaene" @click="isMobileMenuOpen = false" <NuxtLink to="/mannschaften/spielplaene" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Spielpläne Spielpläne
</NuxtLink> </NuxtLink>
<NuxtLink to="/spielsysteme" @click="isMobileMenuOpen = false" <NuxtLink to="/spielsysteme" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Spielsysteme Spielsysteme
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
<!-- Training Mobile --> <!-- Training Mobile -->
<div> <div>
@@ -387,16 +365,16 @@
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Trainer Trainer
</NuxtLink> </NuxtLink>
<NuxtLink to="/training/anfaenger" @click="isMobileMenuOpen = false" <NuxtLink to="/training/anfaenger" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Anfänger Anfänger
</NuxtLink> </NuxtLink>
<NuxtLink to="/tt-regeln" @click="isMobileMenuOpen = false" <NuxtLink to="/tt-regeln" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"> class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
TT-Regeln TT-Regeln
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
<NuxtLink to="/mitgliedschaft" @click="isMobileMenuOpen = false" <NuxtLink to="/mitgliedschaft" @click="isMobileMenuOpen = false"
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors"> class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
@@ -408,10 +386,7 @@
Termine Termine
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink v-if="hasGalleryImages" to="/galerie" @click="isMobileMenuOpen = false"
v-if="hasGalleryImages"
to="/galerie"
@click="isMobileMenuOpen = false"
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors"> class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
Galerie Galerie
</NuxtLink> </NuxtLink>
@@ -526,24 +501,24 @@ const showCmsDropdown = ref(false)
const isLoggedIn = computed(() => authStore.isLoggedIn) const isLoggedIn = computed(() => authStore.isLoggedIn)
const isAdmin = computed(() => authStore.isAdmin) const isAdmin = computed(() => authStore.isAdmin)
// Automatisches Setzen des Submenus basierend auf der Route // Automatisches Setzen des Submenus basierend auf der Route
const currentSubmenu = computed(() => { const currentSubmenu = computed(() => {
const path = route.path const path = route.path
if (path.startsWith('/verein/') || path.startsWith('/vorstand') || if (path.startsWith('/verein/') || path.startsWith('/vorstand') ||
path.startsWith('/vereinsmeisterschaften')) { path.startsWith('/vereinsmeisterschaften')) {
return 'verein' return 'verein'
} }
if (path.startsWith('/mannschaften') || path.startsWith('/spielsysteme')) { if (path.startsWith('/mannschaften') || path.startsWith('/spielsysteme')) {
return 'mannschaften' return 'mannschaften'
} }
if (path.startsWith('/training') || path.startsWith('/tt-regeln')) { if (path.startsWith('/training') || path.startsWith('/tt-regeln')) {
return 'training' return 'training'
} }
if (path.startsWith('/mitgliederbereich') || path.startsWith('/cms')) { if (path.startsWith('/mitgliederbereich') || path.startsWith('/cms')) {
return 'intern' return 'intern'
} }
return null return null
}) })
// Manuelles Toggle für Click-Events // Manuelles Toggle für Click-Events
const manualSubmenu = ref(null) const manualSubmenu = ref(null)
@@ -621,19 +596,19 @@ onMounted(() => {
}) })
}) })
const toggleSubmenu = (menu) => { const toggleSubmenu = (menu) => {
// Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen) // Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen)
// Wenn nicht, zur Hauptseite navigieren // Wenn nicht, zur Hauptseite navigieren
const path = route.path const path = route.path
if (menu === 'verein' && !path.startsWith('/verein/') && !path.startsWith('/vorstand') && !path.startsWith('/vereinsmeisterschaften')) { if (menu === 'verein' && !path.startsWith('/verein/') && !path.startsWith('/vorstand') && !path.startsWith('/vereinsmeisterschaften')) {
navigateTo('/verein/ueber-uns') navigateTo('/verein/ueber-uns')
} else if (menu === 'mannschaften' && !path.startsWith('/mannschaften') && !path.startsWith('/spielsysteme')) { } else if (menu === 'mannschaften' && !path.startsWith('/mannschaften') && !path.startsWith('/spielsysteme')) {
navigateTo('/mannschaften') navigateTo('/mannschaften')
} else if (menu === 'training' && !path.startsWith('/training') && !path.startsWith('/tt-regeln')) { } else if (menu === 'training' && !path.startsWith('/training') && !path.startsWith('/tt-regeln')) {
navigateTo('/training') navigateTo('/training')
} else if (menu === 'intern' && !path.startsWith('/mitgliederbereich') && !path.startsWith('/cms')) { } else if (menu === 'intern' && !path.startsWith('/mitgliederbereich') && !path.startsWith('/cms')) {
navigateTo('/mitgliederbereich') navigateTo('/mitgliederbereich')
} }
} }
</script> </script>

View File

@@ -0,0 +1,549 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
API-Dokumentation
</h1>
<div class="w-24 h-1 bg-primary-600 mb-6" />
<p class="text-xl text-gray-600">
Übersicht über alle verfügbaren API-Endpoints und deren Verwendung
</p>
</div>
<!-- Authentication Info -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 rounded-lg mb-8">
<h2 class="text-xl font-semibold text-blue-900 mb-2">Authentifizierung</h2>
<p class="text-blue-800 mb-4">
Alle API-Endpoints erfordern Authentifizierung (außer Login). Es werden zwei Methoden unterstützt:
</p>
<div class="space-y-3">
<div>
<strong class="text-blue-900">1. Cookie-basiert:</strong>
<p class="text-blue-700 text-sm mt-1">Nach dem Login über <code>/api/auth/login</code> wird automatisch ein Cookie gesetzt.</p>
</div>
<div>
<strong class="text-blue-900">2. Authorization Header:</strong>
<p class="text-blue-700 text-sm mt-1">Header: <code>Authorization: Bearer &lt;token&gt;</code></p>
<p class="text-blue-700 text-sm">Der Token wird im Login-Response im Feld <code>token</code> zurückgegeben.</p>
</div>
</div>
</div>
<!-- Endpoints -->
<div class="space-y-8">
<!-- Authentication Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Authentifizierung</h2>
<div class="space-y-6">
<!-- Login -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/auth/login</h3>
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded">Öffentlich</span>
</div>
<p class="text-gray-600 mb-3">Benutzer einloggen und Token erhalten</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"email": "benutzer@example.com",
"password": "passwort"
}</code></pre>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user-id",
"email": "benutzer@example.com",
"name": "Max Mustermann",
"role": "mitglied"
}
}</code></pre>
</div>
</div>
<!-- Logout -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/auth/logout</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Benutzer ausloggen</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Erfolgreich ausgeloggt"
}</code></pre>
</div>
</div>
<!-- Auth Status -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/auth/status</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Aktuellen Authentifizierungsstatus abrufen</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"isLoggedIn": true,
"user": {
"id": "user-id",
"email": "benutzer@example.com",
"name": "Max Mustermann",
"role": "mitglied"
},
"role": "mitglied"
}</code></pre>
</div>
</div>
</div>
</section>
<!-- Members Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Mitglieder</h2>
<div class="space-y-6">
<!-- Get Members -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/members</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Alle Mitglieder abrufen (mit Merge aus registrierten Benutzern)</p>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"members": [
{
"id": "member-id",
"firstName": "Max",
"lastName": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.com",
"phone": "0123456789",
"address": "Musterstraße 1",
"source": "manual",
"editable": true,
"hasLogin": false
}
]
}</code></pre>
</div>
</div>
<!-- Post Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/members</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neues Mitglied hinzufügen oder bestehendes bearbeiten</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "optional-für-update",
"firstName": "Max",
"lastName": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.com",
"phone": "0123456789",
"address": "Musterstraße 1, 12345 Musterstadt",
"notes": "Optional"
}</code></pre>
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Mitglied erfolgreich gespeichert."
}</code></pre>
</div>
<div class="mt-3 bg-blue-50 rounded-lg p-3">
<p class="text-xs text-blue-800">
<strong>Hinweis:</strong> Ohne <code>id</code> wird ein neues Mitglied erstellt. Mit <code>id</code> wird ein bestehendes Mitglied aktualisiert. <code>geburtsdatum</code> ist Pflichtfeld zur Duplikatsprüfung (Format: YYYY-MM-DD).
</p>
</div>
</div>
<!-- Bulk Import Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/members/bulk</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Mehrere Mitglieder auf einmal importieren (Bulk-Import)</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"members": [
{
"firstName": "Max",
"lastName": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.com",
"phone": "0123456789",
"address": "Musterstraße 1",
"notes": "Optional"
},
{
"firstName": "Anna",
"lastName": "Schmidt",
"geburtsdatum": "1985-03-20",
"email": "anna@example.com"
}
]
}</code></pre>
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"summary": {
"total": 2,
"imported": 2,
"duplicates": 0,
"errors": 0
},
"results": {
"success": [
{
"index": 1,
"member": { ... }
}
],
"duplicates": [],
"errors": []
}
}</code></pre>
</div>
<div class="mt-3 bg-blue-50 rounded-lg p-3">
<p class="text-xs text-blue-800 mb-2">
<strong>Features:</strong>
</p>
<ul class="text-xs text-blue-700 list-disc list-inside space-y-1">
<li>Duplikatsprüfung gegen bestehende Mitglieder</li>
<li>Duplikatsprüfung innerhalb des Imports</li>
<li>Validierung aller Daten vor dem Import</li>
<li>Detaillierte Fehlerberichte für jeden Eintrag</li>
<li>Nur erfolgreiche Einträge werden gespeichert</li>
</ul>
</div>
</div>
<!-- Delete Members -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/members</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Mitglied löschen</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "member-id"
}</code></pre>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Response:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"success": true,
"message": "Mitglied erfolgreich gelöscht."
}</code></pre>
</div>
</div>
</div>
</section>
<!-- News Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">News</h2>
<div class="space-y-6">
<!-- Get News -->
<div class="border-l-4 border-primary-600 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/news</h3>
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
</div>
<p class="text-gray-600 mb-3">Alle News abrufen (inkl. interner News)</p>
</div>
<!-- Post News -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/news</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neue News erstellen oder bestehende bearbeiten</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "optional-für-update",
"title": "Titel der News",
"content": "Inhalt der News",
"isPublic": true,
"expiresAt": "2025-12-31T23:59:59.000Z",
"isHidden": false
}</code></pre>
</div>
</div>
<!-- Delete News -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/news</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">News löschen</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"id": "news-id"
}</code></pre>
</div>
</div>
</div>
</section>
<!-- Termine Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Termine</h2>
<div class="space-y-6">
<!-- Get Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/termine-manage</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Alle Termine abrufen (für Verwaltung)</p>
</div>
<!-- Post Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">POST /api/termine-manage</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Neuen Termin erstellen</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>{
"datum": "2025-12-25",
"uhrzeit": "19:00",
"titel": "Weihnachtsfeier",
"beschreibung": "Gemeinsame Feier",
"kategorie": "Veranstaltung"
}</code></pre>
</div>
</div>
<!-- Delete Termine -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">DELETE /api/termine-manage</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Termin löschen</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Query Parameters:</p>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto"><code>?datum=2025-12-25&uhrzeit=19:00&titel=Weihnachtsfeier&beschreibung=...&kategorie=...</code></pre>
</div>
</div>
</div>
</section>
<!-- Config Endpoints -->
<section class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Konfiguration</h2>
<div class="space-y-6">
<!-- Get Config -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">GET /api/config</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Vereinskonfiguration abrufen</p>
</div>
<!-- Put Config -->
<div class="border-l-4 border-red-500 pl-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">PUT /api/config</h3>
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
</div>
<p class="text-gray-600 mb-3">Vereinskonfiguration aktualisieren</p>
<div class="bg-gray-50 rounded-lg p-4 mb-3">
<p class="text-sm font-medium text-gray-700 mb-2">Request Body:</p>
<p class="text-xs text-gray-600">Komplettes Config-Objekt mit allen Einstellungen</p>
</div>
</div>
</div>
</section>
</div>
<!-- Example Usage -->
<div class="mt-12 bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">Beispiel-Usage</h2>
<div class="space-y-4">
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">cURL Beispiel:</h3>
<pre class="text-xs bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code># Login und Token erhalten
curl -X POST http://localhost:3100/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "passwort"}'
# Mitglied hinzufügen mit Token
curl -X POST http://localhost:3100/api/members \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Max",
"lastName": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.com",
"phone": "0123456789"
}'
# Bulk-Import von Mitgliedern
curl -X POST http://localhost:3100/api/members/bulk \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"members": [
{
"firstName": "Max",
"lastName": "Mustermann",
"geburtsdatum": "1990-01-15",
"email": "max@example.com"
},
{
"firstName": "Anna",
"lastName": "Schmidt",
"geburtsdatum": "1985-03-20",
"email": "anna@example.com"
}
]
}'</code></pre>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">JavaScript/Fetch Beispiel:</h3>
<pre class="text-xs bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code>// Login
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'admin@example.com',
password: 'passwort'
})
})
const { token } = await loginResponse.json()
// Mitglied hinzufügen
const memberResponse = await fetch('/api/members', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstName: 'Max',
lastName: 'Mustermann',
geburtsdatum: '1990-01-15',
email: 'max@example.com'
})
})
// Bulk-Import
const bulkResponse = await fetch('/api/members/bulk', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
members: [
{
firstName: 'Max',
lastName: 'Mustermann',
geburtsdatum: '1990-01-15',
email: 'max@example.com'
},
{
firstName: 'Anna',
lastName: 'Schmidt',
geburtsdatum: '1985-03-20',
email: 'anna@example.com'
}
]
})
})
const result = await bulkResponse.json()
console.log(`Importiert: ${result.summary.imported}, Duplikate: ${result.summary.duplicates}`)</code></pre>
</div>
</div>
</div>
<!-- Role Legend -->
<div class="mt-8 bg-gray-50 rounded-xl p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Legende</h2>
<div class="grid md:grid-cols-3 gap-4">
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded">Öffentlich</span>
<span class="text-sm text-gray-600">Keine Authentifizierung erforderlich</span>
</div>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded">Auth erforderlich</span>
<span class="text-sm text-gray-600">Jeder eingeloggte Benutzer</span>
</div>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded">admin/vorstand</span>
<span class="text-sm text-gray-600">Nur Admin oder Vorstand</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'auth',
layout: 'default'
})
useHead({
title: 'API-Dokumentation - Harheimer TC',
})
</script>

View File

@@ -16,6 +16,16 @@
<component :is="viewMode === 'cards' ? Table2 : Grid3x3" :size="20" class="mr-2" /> <component :is="viewMode === 'cards' ? Table2 : Grid3x3" :size="20" class="mr-2" />
{{ viewMode === 'cards' ? 'Tabelle' : 'Karten' }} {{ viewMode === 'cards' ? 'Tabelle' : 'Karten' }}
</button> </button>
<button
v-if="canEdit"
@click="showBulkImportModal = true"
class="flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Bulk-Import
</button>
<button <button
v-if="canEdit" v-if="canEdit"
@click="openAddModal" @click="openAddModal"
@@ -234,6 +244,18 @@
</div> </div>
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Geburtsdatum *</label>
<input
v-model="formData.geburtsdatum"
type="date"
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
:disabled="isSaving"
/>
<p class="text-xs text-gray-500 mt-1">Wird zur eindeutigen Identifizierung benötigt</p>
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">E-Mail</label> <label class="block text-sm font-medium text-gray-700 mb-2">E-Mail</label>
<input <input
@@ -300,6 +322,144 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Bulk Import Modal -->
<div
v-if="showBulkImportModal"
class="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
@click.self="closeBulkImportModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto p-8">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Bulk-Import von Mitgliedern
</h2>
<!-- CSV Upload Section -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">CSV-Datei hochladen</label>
<div
@click="triggerBulkFileInput"
@dragover.prevent
@dragenter.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false"
@drop.prevent="handleBulkFileDrop"
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer"
:class="{ 'border-primary-400 bg-primary-50': isDragOver }"
>
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-lg font-medium text-gray-900 mb-2">CSV-Datei hochladen</p>
<p class="text-sm text-gray-600 mb-4">Klicken Sie hier oder ziehen Sie eine CSV-Datei hierher</p>
<p v-if="bulkSelectedFile" class="text-sm text-primary-600 font-medium">{{ bulkSelectedFile.name }}</p>
</div>
<input
ref="bulkFileInput"
type="file"
accept=".csv"
@change="handleBulkFileSelect"
class="hidden"
/>
</div>
<!-- CSV Format Info -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-lg mb-6">
<h4 class="text-sm font-medium text-blue-800 mb-2">Erwartetes CSV-Format:</h4>
<div class="text-xs text-blue-700 space-y-1">
<p> Erste Zeile: Spaltenüberschriften (firstName, lastName, geburtsdatum, email, phone, address, notes)</p>
<p> <strong>Pflichtfelder:</strong> firstName, lastName, geburtsdatum</p>
<p> <strong>Geburtsdatum:</strong> Format YYYY-MM-DD (z.B. 1990-01-15)</p>
<p> Trennzeichen: Komma (,) oder Semikolon (;)</p>
</div>
</div>
<!-- Preview Section -->
<div v-if="bulkPreviewData.length > 0" class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vorschau ({{ bulkPreviewData.length }} Einträge)</h3>
<div class="max-h-64 overflow-y-auto border border-gray-200 rounded-lg">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Vorname</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Nachname</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Geburtsdatum</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">E-Mail</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(row, index) in bulkPreviewData.slice(0, 10)" :key="index" class="hover:bg-gray-50">
<td class="px-3 py-2">{{ row.firstName || '-' }}</td>
<td class="px-3 py-2">{{ row.lastName || '-' }}</td>
<td class="px-3 py-2">{{ row.geburtsdatum || '-' }}</td>
<td class="px-3 py-2">{{ row.email || '-' }}</td>
</tr>
</tbody>
</table>
<div v-if="bulkPreviewData.length > 10" class="px-3 py-2 text-xs text-gray-500 bg-gray-50 text-center">
... und {{ bulkPreviewData.length - 10 }} weitere
</div>
</div>
</div>
<!-- Import Results -->
<div v-if="bulkImportResults" class="mb-6">
<div class="bg-gray-50 rounded-lg p-4">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Import-Ergebnisse</h3>
<div class="grid grid-cols-3 gap-4 mb-4">
<div class="text-center">
<div class="text-2xl font-bold text-green-600">{{ bulkImportResults.summary.imported }}</div>
<div class="text-sm text-gray-600">Importiert</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-yellow-600">{{ bulkImportResults.summary.duplicates }}</div>
<div class="text-sm text-gray-600">Duplikate</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-red-600">{{ bulkImportResults.summary.errors }}</div>
<div class="text-sm text-gray-600">Fehler</div>
</div>
</div>
<div v-if="bulkImportResults.results.duplicates.length > 0" class="mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Duplikate:</h4>
<div class="text-xs text-gray-600 space-y-1 max-h-32 overflow-y-auto">
<div v-for="dup in bulkImportResults.results.duplicates" :key="dup.index">
Zeile {{ dup.index }}: {{ dup.member.firstName }} {{ dup.member.lastName }} - {{ dup.reason }}
</div>
</div>
</div>
<div v-if="bulkImportResults.results.errors.length > 0" class="mt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Fehler:</h4>
<div class="text-xs text-red-600 space-y-1 max-h-32 overflow-y-auto">
<div v-for="err in bulkImportResults.results.errors" :key="err.index">
Zeile {{ err.index }}: {{ err.error }}
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end space-x-4 pt-4">
<button
type="button"
@click="closeBulkImportModal"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
:disabled="isBulkImporting"
>
Schließen
</button>
<button
@click="processBulkImport"
:disabled="!bulkPreviewData.length || isBulkImporting"
class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors flex items-center disabled:bg-gray-400"
>
<Loader2 v-if="isBulkImporting" :size="20" class="animate-spin mr-2" />
<span>{{ isBulkImporting ? 'Importiert...' : 'Importieren' }}</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -318,9 +478,19 @@ const editingMember = ref(null)
const errorMessage = ref('') const errorMessage = ref('')
const viewMode = ref('cards') // 'table' or 'cards' const viewMode = ref('cards') // 'table' or 'cards'
// Bulk import state
const showBulkImportModal = ref(false)
const bulkFileInput = ref(null)
const bulkSelectedFile = ref(null)
const bulkPreviewData = ref([])
const isBulkImporting = ref(false)
const bulkImportResults = ref(null)
const isDragOver = ref(false)
const formData = ref({ const formData = ref({
firstName: '', firstName: '',
lastName: '', lastName: '',
geburtsdatum: '',
email: '', email: '',
phone: '', phone: '',
address: '', address: '',
@@ -355,6 +525,7 @@ const openAddModal = () => {
formData.value = { formData.value = {
firstName: '', firstName: '',
lastName: '', lastName: '',
geburtsdatum: '',
email: '', email: '',
phone: '', phone: '',
address: '', address: '',
@@ -369,6 +540,7 @@ const openEditModal = (member) => {
formData.value = { formData.value = {
firstName: member.firstName || '', firstName: member.firstName || '',
lastName: member.lastName || '', lastName: member.lastName || '',
geburtsdatum: member.geburtsdatum || '',
email: member.email || '', email: member.email || '',
phone: member.phone || '', phone: member.phone || '',
address: member.address || '', address: member.address || '',
@@ -399,8 +571,18 @@ const saveMember = async () => {
closeModal() closeModal()
await loadMembers() await loadMembers()
if (window.showSuccessModal) {
window.showSuccessModal('Erfolg', 'Mitglied erfolgreich gespeichert.')
}
} catch (error) { } catch (error) {
errorMessage.value = error.data?.message || 'Fehler beim Speichern des Mitglieds.' console.error('Fehler beim Speichern:', error)
const errorMsg = error.data?.message || error.message || 'Fehler beim Speichern des Mitglieds.'
errorMessage.value = errorMsg
// If it's a duplicate error (409), show it prominently
if ((error.statusCode === 409 || error.status === 409) && window.showErrorModal) {
window.showErrorModal('Duplikat gefunden', errorMsg)
}
} finally { } finally {
isSaving.value = false isSaving.value = false
} }
@@ -435,6 +617,131 @@ const formatDate = (dateString) => {
}) })
} }
// Bulk import functions
const triggerBulkFileInput = () => {
bulkFileInput.value?.click()
}
const handleBulkFileSelect = (event) => {
const file = event.target.files?.[0]
if (file) {
processBulkCSV(file)
}
}
const handleBulkFileDrop = (event) => {
isDragOver.value = false
const file = event.dataTransfer?.files?.[0]
if (file && file.type === 'text/csv') {
processBulkCSV(file)
}
}
const processBulkCSV = async (file) => {
bulkSelectedFile.value = file
bulkImportResults.value = null
try {
const text = await file.text()
const lines = text.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) {
window.showErrorModal('Fehler', 'CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten')
return
}
// Detect delimiter
const parseCSVLine = (line) => {
const tabCount = (line.match(/\t/g) || []).length
const semicolonCount = (line.match(/;/g) || []).length
const delimiter = tabCount > semicolonCount ? '\t' : (semicolonCount > 0 ? ';' : ',')
return line.split(delimiter).map(value => value.trim().replace(/^"|"$/g, ''))
}
// Parse header
const headers = parseCSVLine(lines[0]).map(h => h.toLowerCase())
// Find column indices
const firstNameIdx = headers.findIndex(h => h.includes('firstname') || h.includes('vorname'))
const lastNameIdx = headers.findIndex(h => h.includes('lastname') || h.includes('nachname'))
const geburtsdatumIdx = headers.findIndex(h => h.includes('geburtsdatum') || h.includes('birthdate') || h.includes('geburt'))
const emailIdx = headers.findIndex(h => h.includes('email') || h.includes('e-mail'))
const phoneIdx = headers.findIndex(h => h.includes('phone') || h.includes('telefon') || h.includes('tel'))
const addressIdx = headers.findIndex(h => h.includes('address') || h.includes('adresse'))
const notesIdx = headers.findIndex(h => h.includes('note') || h.includes('notiz') || h.includes('bemerkung'))
if (firstNameIdx === -1 || lastNameIdx === -1 || geburtsdatumIdx === -1) {
window.showErrorModal('Fehler', 'CSV muss Spalten für firstName, lastName und geburtsdatum enthalten')
return
}
// Parse data rows
bulkPreviewData.value = lines.slice(1).map((line, index) => {
const values = parseCSVLine(line)
return {
firstName: values[firstNameIdx] || '',
lastName: values[lastNameIdx] || '',
geburtsdatum: values[geburtsdatumIdx] || '',
email: emailIdx !== -1 ? (values[emailIdx] || '') : '',
phone: phoneIdx !== -1 ? (values[phoneIdx] || '') : '',
address: addressIdx !== -1 ? (values[addressIdx] || '') : '',
notes: notesIdx !== -1 ? (values[notesIdx] || '') : ''
}
}).filter(row => row.firstName && row.lastName && row.geburtsdatum)
} catch (error) {
console.error('Fehler beim Parsen der CSV:', error)
window.showErrorModal('Fehler', 'Fehler beim Lesen der CSV-Datei: ' + error.message)
}
}
const processBulkImport = async () => {
if (!bulkPreviewData.value.length) return
isBulkImporting.value = true
bulkImportResults.value = null
try {
const response = await $fetch('/api/members/bulk', {
method: 'POST',
body: {
members: bulkPreviewData.value
}
})
bulkImportResults.value = response
if (response.summary.imported > 0) {
await loadMembers()
window.showSuccessModal(
'Import erfolgreich',
`${response.summary.imported} Mitglieder wurden erfolgreich importiert.`
)
}
if (response.summary.duplicates > 0 || response.summary.errors > 0) {
// Results are already displayed in the modal
}
} catch (error) {
console.error('Fehler beim Bulk-Import:', error)
const errorMsg = error.data?.message || error.message || 'Fehler beim Import'
window.showErrorModal('Import-Fehler', errorMsg)
} finally {
isBulkImporting.value = false
}
}
const closeBulkImportModal = () => {
showBulkImportModal.value = false
bulkSelectedFile.value = null
bulkPreviewData.value = []
bulkImportResults.value = null
isDragOver.value = false
if (bulkFileInput.value) {
bulkFileInput.value.value = ''
}
}
onMounted(() => { onMounted(() => {
loadMembers() loadMembers()
}) })

View File

@@ -59,9 +59,10 @@ export default defineEventHandler(async (event) => {
maxAge: 60 * 60 * 24 * 7 // 7 days maxAge: 60 * 60 * 24 * 7 // 7 days
}) })
// Return user data (without password) // Return user data (without password) and token for API usage
return { return {
success: true, success: true,
token: token, // Token auch im Body für externe API-Clients
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,

View File

@@ -3,12 +3,21 @@ import { saveMember } from '../utils/members.js'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const token = getCookie(event, 'auth_token') // Support both Cookie and Authorization Header
let token = getCookie(event, 'auth_token')
// If no cookie token, try Authorization header (Bearer token)
if (!token) {
const authHeader = getHeader(event, 'authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7)
}
}
if (!token) { if (!token) {
throw createError({ throw createError({
statusCode: 401, statusCode: 401,
message: 'Nicht authentifiziert.' message: 'Nicht authentifiziert. Bitte Token im Cookie oder Authorization-Header bereitstellen.'
}) })
} }
@@ -23,16 +32,23 @@ export default defineEventHandler(async (event) => {
const user = await getUserById(decoded.id) const user = await getUserById(decoded.id)
// Only admin and vorstand can edit members if (!user) {
if (!user || (user.role !== 'admin' && user.role !== 'vorstand')) { throw createError({
statusCode: 401,
message: 'Benutzer nicht gefunden.'
})
}
// Only admin and vorstand can add/edit members
if (user.role !== 'admin' && user.role !== 'vorstand') {
throw createError({ throw createError({
statusCode: 403, statusCode: 403,
message: 'Keine Berechtigung zum Bearbeiten von Mitgliedern.' message: 'Keine Berechtigung zum Hinzufügen/Bearbeiten von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
}) })
} }
const body = await readBody(event) const body = await readBody(event)
const { id, firstName, lastName, email, phone, address, notes } = body const { id, firstName, lastName, geburtsdatum, email, phone, address, notes } = body
if (!firstName || !lastName) { if (!firstName || !lastName) {
throw createError({ throw createError({
@@ -41,23 +57,51 @@ export default defineEventHandler(async (event) => {
}) })
} }
await saveMember({ if (!geburtsdatum) {
id: id || undefined, throw createError({
firstName, statusCode: 400,
lastName, message: 'Geburtsdatum ist erforderlich, um Duplikate zu vermeiden.'
email: email || '', })
phone: phone || '', }
address: address || '',
notes: notes || ''
})
return { try {
success: true, await saveMember({
message: 'Mitglied erfolgreich gespeichert.' id: id || undefined,
firstName,
lastName,
geburtsdatum: geburtsdatum || '',
email: email || '',
phone: phone || '',
address: address || '',
notes: notes || ''
})
return {
success: true,
message: 'Mitglied erfolgreich gespeichert.'
}
} catch (memberError) {
// Check if it's a duplicate error
if (memberError.message && memberError.message.includes('existiert bereits')) {
throw createError({
statusCode: 409,
message: memberError.message
})
}
// Re-throw other errors
throw memberError
} }
} catch (error) { } catch (error) {
console.error('Fehler beim Speichern des Mitglieds:', error) console.error('Fehler beim Speichern des Mitglieds:', error)
throw error // If it's already a createError, re-throw it
if (error.statusCode) {
throw error
}
// Otherwise wrap it
throw createError({
statusCode: error.statusCode || 500,
message: error.message || 'Fehler beim Speichern des Mitglieds.'
})
} }
}) })

View File

@@ -0,0 +1,192 @@
import { verifyToken, getUserById } from '../../utils/auth.js'
import { readMembers, writeMembers, normalizeDate } from '../../utils/members.js'
import { randomUUID } from 'crypto'
// Helper function to check for duplicates in a list (with optional exclude)
function findDuplicateMemberInList(members, firstName, lastName, geburtsdatum, excludeId = null) {
const normalizedFirstName = (firstName || '').trim().toLowerCase()
const normalizedLastName = (lastName || '').trim().toLowerCase()
const normalizedDate = normalizeDate(geburtsdatum)
return members.find(m => {
if (excludeId && m.id === excludeId) return false
const mFirstName = (m.firstName || '').trim().toLowerCase()
const mLastName = (m.lastName || '').trim().toLowerCase()
const mDate = normalizeDate(m.geburtsdatum)
return mFirstName === normalizedFirstName &&
mLastName === normalizedLastName &&
mDate === normalizedDate &&
mDate !== ''
})
}
export default defineEventHandler(async (event) => {
try {
// Support both Cookie and Authorization Header
let token = getCookie(event, 'auth_token')
if (!token) {
const authHeader = getHeader(event, 'authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7)
}
}
if (!token) {
throw createError({
statusCode: 401,
message: 'Nicht authentifiziert. Bitte Token im Cookie oder Authorization-Header bereitstellen.'
})
}
const decoded = verifyToken(token)
if (!decoded) {
throw createError({
statusCode: 401,
message: 'Ungültiges Token.'
})
}
const user = await getUserById(decoded.id)
if (!user) {
throw createError({
statusCode: 401,
message: 'Benutzer nicht gefunden.'
})
}
// Only admin and vorstand can add members in bulk
if (user.role !== 'admin' && user.role !== 'vorstand') {
throw createError({
statusCode: 403,
message: 'Keine Berechtigung zum Bulk-Import von Mitgliedern. Erforderlich: admin oder vorstand Rolle.'
})
}
const body = await readBody(event)
const { members: membersToImport } = body
if (!Array.isArray(membersToImport) || membersToImport.length === 0) {
throw createError({
statusCode: 400,
message: 'Bitte senden Sie ein Array von Mitgliedern im Feld "members".'
})
}
// Validate all members before processing
const validationErrors = []
membersToImport.forEach((member, index) => {
if (!member.firstName || !member.lastName) {
validationErrors.push(`Zeile ${index + 1}: Vorname und Nachname sind erforderlich.`)
}
if (!member.geburtsdatum) {
validationErrors.push(`Zeile ${index + 1}: Geburtsdatum ist erforderlich.`)
}
})
if (validationErrors.length > 0) {
throw createError({
statusCode: 400,
message: `Validierungsfehler:\n${validationErrors.join('\n')}`
})
}
// Read existing members
const existingMembers = await readMembers()
const results = {
success: [],
duplicates: [],
errors: []
}
// Process each member
for (let i = 0; i < membersToImport.length; i++) {
const memberData = membersToImport[i]
try {
// Check for duplicates in existing members
const duplicateInExisting = findDuplicateMemberInList(
existingMembers,
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
)
if (duplicateInExisting) {
results.duplicates.push({
index: i + 1,
member: memberData,
reason: `Existiert bereits (ID: ${duplicateInExisting.id})`
})
continue
}
// Check for duplicates within the import batch
const duplicateInBatch = findDuplicateMemberInList(
membersToImport.slice(0, i),
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
)
if (duplicateInBatch) {
results.duplicates.push({
index: i + 1,
member: memberData,
reason: 'Duplikat innerhalb des Imports'
})
continue
}
// Add new member
const newMember = {
...memberData,
id: memberData.id || randomUUID()
}
existingMembers.push(newMember)
results.success.push({
index: i + 1,
member: newMember
})
} catch (error) {
results.errors.push({
index: i + 1,
member: memberData,
error: error.message || 'Unbekannter Fehler'
})
}
}
// Save all successfully imported members
if (results.success.length > 0) {
await writeMembers(existingMembers)
}
return {
success: results.success.length > 0,
summary: {
total: membersToImport.length,
imported: results.success.length,
duplicates: results.duplicates.length,
errors: results.errors.length
},
results: results
}
} catch (error) {
console.error('Fehler beim Bulk-Import von Mitgliedern:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: error.statusCode || 500,
message: error.message || 'Fehler beim Bulk-Import von Mitgliedern.'
})
}
})

View File

@@ -106,6 +106,37 @@ export async function getMemberById(id) {
return members.find(m => m.id === id) return members.find(m => m.id === id)
} }
// Normalize date string for comparison (handles different date formats)
export function normalizeDate(dateString) {
if (!dateString) return ''
// Try to parse and normalize to ISO format (YYYY-MM-DD)
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) return dateString.trim()
return date.toISOString().split('T')[0]
} catch (e) {
return dateString.trim()
}
}
// Check for duplicate member based on firstName, lastName, and geburtsdatum
function findDuplicateMember(members, firstName, lastName, geburtsdatum) {
const normalizedFirstName = (firstName || '').trim().toLowerCase()
const normalizedLastName = (lastName || '').trim().toLowerCase()
const normalizedDate = normalizeDate(geburtsdatum)
return members.find(m => {
const mFirstName = (m.firstName || '').trim().toLowerCase()
const mLastName = (m.lastName || '').trim().toLowerCase()
const mDate = normalizeDate(m.geburtsdatum)
return mFirstName === normalizedFirstName &&
mLastName === normalizedLastName &&
mDate === normalizedDate &&
mDate !== '' // Only match if date is provided for both
})
}
// Add or update manual member // Add or update manual member
export async function saveMember(memberData) { export async function saveMember(memberData) {
const members = await readMembers() const members = await readMembers()
@@ -114,11 +145,37 @@ export async function saveMember(memberData) {
// Update existing // Update existing
const index = members.findIndex(m => m.id === memberData.id) const index = members.findIndex(m => m.id === memberData.id)
if (index !== -1) { if (index !== -1) {
// Check for duplicate (excluding current member)
const duplicate = findDuplicateMember(
members.filter(m => m.id !== memberData.id),
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
)
if (duplicate) {
throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.')
}
members[index] = { ...members[index], ...memberData } members[index] = { ...members[index], ...memberData }
} else { } else {
throw new Error('Mitglied nicht gefunden') throw new Error('Mitglied nicht gefunden')
} }
} else { } else {
// Add new - check for duplicate first
if (memberData.firstName && memberData.lastName && memberData.geburtsdatum) {
const duplicate = findDuplicateMember(
members,
memberData.firstName,
memberData.lastName,
memberData.geburtsdatum
)
if (duplicate) {
throw new Error('Ein Mitglied mit diesem Namen und Geburtsdatum existiert bereits.')
}
}
// Add new - use UUID for guaranteed uniqueness // Add new - use UUID for guaranteed uniqueness
const newMember = { const newMember = {
...memberData, ...memberData,