Fehlerbehebung beim Timelock, Neues Feature Keyholder finden
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#Wed Mar 25 21:32:43 CET 2026
|
#Thu Mar 26 16:50:07 CET 2026
|
||||||
display=\:0
|
display=\:0
|
||||||
host=mario-mint
|
host=mario-mint
|
||||||
process-id=43084
|
process-id=121337
|
||||||
user=mario
|
user=mario
|
||||||
|
|||||||
284
.metadata/.log
284
.metadata/.log
@@ -819,3 +819,287 @@ Binding(CTRL+R,
|
|||||||
,,true),null),
|
,,true),null),
|
||||||
org.eclipse.ui.defaultAcceleratorConfiguration,
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
org.eclipse.debug.ui.console,,,system)
|
org.eclipse.debug.ui.console,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-26 00:13:15.217
|
||||||
|
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||||
|
!SESSION 2026-03-26 07:50:09.934 -----------------------------------------------
|
||||||
|
eclipse.buildId=4.39.0.20260305-0817
|
||||||
|
java.version=21.0.6
|
||||||
|
java.vendor=Eclipse Adoptium
|
||||||
|
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||||
|
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||||
|
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 07:50:11.470
|
||||||
|
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 07:50:14.978
|
||||||
|
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 07:50:15.138
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 07:50:15.138
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 07:50:15.273
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 07:50:15.273
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.debug.core 4 125 2026-03-26 07:52:41.947
|
||||||
|
!MESSAGE Error logged from Debug Core:
|
||||||
|
!STACK 0
|
||||||
|
java.io.IOException: Stream closed
|
||||||
|
at java.base/java.io.BufferedInputStream.getBufIfOpen(BufferedInputStream.java:188)
|
||||||
|
at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:343)
|
||||||
|
at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420)
|
||||||
|
at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:405)
|
||||||
|
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95)
|
||||||
|
at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235)
|
||||||
|
at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211)
|
||||||
|
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.jface 2 0 2026-03-26 07:52:44.356
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 07:52:44.356
|
||||||
|
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||||
|
Open a type in a Java editor,
|
||||||
|
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||||
|
,
|
||||||
|
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-26 10:28:33.777
|
||||||
|
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||||
|
!SESSION 2026-03-26 10:32:16.891 -----------------------------------------------
|
||||||
|
eclipse.buildId=4.39.0.20260305-0817
|
||||||
|
java.version=21.0.6
|
||||||
|
java.vendor=Eclipse Adoptium
|
||||||
|
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||||
|
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||||
|
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 10:32:18.391
|
||||||
|
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 10:32:21.250
|
||||||
|
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 10:32:21.401
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 10:32:21.401
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 10:32:21.529
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 10:32:21.529
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.jface 2 0 2026-03-26 10:32:54.845
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 10:32:54.845
|
||||||
|
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||||
|
Open a type in a Java editor,
|
||||||
|
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||||
|
,
|
||||||
|
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-26 10:47:57.323
|
||||||
|
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||||
|
!SESSION 2026-03-26 11:31:28.220 -----------------------------------------------
|
||||||
|
eclipse.buildId=4.39.0.20260305-0817
|
||||||
|
java.version=21.0.6
|
||||||
|
java.vendor=Eclipse Adoptium
|
||||||
|
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||||
|
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||||
|
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 11:31:29.790
|
||||||
|
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 11:31:36.957
|
||||||
|
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 11:31:37.090
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 11:31:37.090
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 11:31:37.210
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 11:31:37.210
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
!SESSION 2026-03-26 16:50:01.920 -----------------------------------------------
|
||||||
|
eclipse.buildId=4.39.0.20260305-0817
|
||||||
|
java.version=21.0.6
|
||||||
|
java.vendor=Eclipse Adoptium
|
||||||
|
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
|
||||||
|
Framework arguments: -product org.eclipse.epp.package.java.product
|
||||||
|
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 16:50:03.384
|
||||||
|
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-26 16:50:07.703
|
||||||
|
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 16:50:07.810
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 16:50:07.810
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.ui 2 0 2026-03-26 16:50:07.943
|
||||||
|
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
|
||||||
|
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-26 16:50:07.943
|
||||||
|
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.lsp4e 2 0 2026-03-26 16:55:47.890
|
||||||
|
!MESSAGE Javadoc unavailable. Failed to obtain it.
|
||||||
|
!STACK 0
|
||||||
|
java.lang.InterruptedException
|
||||||
|
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
|
||||||
|
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
|
||||||
|
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
|
||||||
|
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
|
||||||
|
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
|
||||||
|
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
|
||||||
|
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.jface 2 0 2026-03-26 19:21:16.922
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 19:21:16.922
|
||||||
|
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||||
|
Open a type in a Java editor,
|
||||||
|
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||||
|
,
|
||||||
|
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.jface 2 0 2026-03-26 20:13:11.892
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-26 20:13:11.892
|
||||||
|
!MESSAGE A conflict occurred for CTRL+R:
|
||||||
|
Binding(CTRL+R,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line,
|
||||||
|
Resume and break when execution reaches the current line,
|
||||||
|
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.debug.ui.debugging,,,system)
|
||||||
|
Binding(CTRL+R,
|
||||||
|
ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart,
|
||||||
|
Restart Spring Boot Application,
|
||||||
|
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.debug.ui.console,,,system)
|
||||||
|
|
||||||
|
!ENTRY Activator 4 0 2026-03-26 22:43:57.249
|
||||||
|
!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null
|
||||||
|
!STACK 0
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.getEnableJmx(BootLaunchConfigurationDelegate.java:426)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:195)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422)
|
||||||
|
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521)
|
||||||
|
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
|
||||||
|
|
||||||
|
!ENTRY Activator 4 0 2026-03-26 22:43:57.250
|
||||||
|
!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null
|
||||||
|
!STACK 0
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProjectName(AbstractBootLaunchConfigurationDelegate.java:322)
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProject(AbstractBootLaunchConfigurationDelegate.java:307)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:196)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422)
|
||||||
|
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521)
|
||||||
|
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
|
||||||
|
|
||||||
|
!ENTRY Activator 4 0 2026-03-26 22:43:57.399
|
||||||
|
!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null
|
||||||
|
!STACK 0
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, boolean)" because "conf" is null
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate.getEnableJmx(BootLaunchConfigurationDelegate.java:426)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:195)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422)
|
||||||
|
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521)
|
||||||
|
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
|
||||||
|
|
||||||
|
!ENTRY Activator 4 0 2026-03-26 22:43:57.399
|
||||||
|
!MESSAGE NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null
|
||||||
|
!STACK 0
|
||||||
|
java.lang.NullPointerException: Cannot invoke "org.eclipse.debug.core.ILaunchConfiguration.getAttribute(String, String)" because "conf" is null
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProjectName(AbstractBootLaunchConfigurationDelegate.java:322)
|
||||||
|
at org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate.getProject(AbstractBootLaunchConfigurationDelegate.java:307)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:196)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.BootDashActivator$1.stateChanged(BootDashActivator.java:1)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.updateOwnerStatesAndFireEvents(RunStateTracker.java:216)
|
||||||
|
at org.springframework.ide.eclipse.boot.dash.util.RunStateTracker.processCreated(RunStateTracker.java:273)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.processCreated(ProcessTracker.java:105)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker.handleDebugEvent(ProcessTracker.java:71)
|
||||||
|
at org.springframework.ide.eclipse.boot.util.ProcessTracker$1.handleDebugEvents(ProcessTracker.java:48)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.run(DebugPlugin.java:1422)
|
||||||
|
at org.eclipse.core.runtime.SafeRunner.run(SafeRunner.java:47)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventNotifier.dispatch(DebugPlugin.java:1456)
|
||||||
|
at org.eclipse.debug.core.DebugPlugin$EventDispatchJob.run(DebugPlugin.java:521)
|
||||||
|
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[ {
|
[ {
|
||||||
"version" : "9.5.0-20260325015243+0000",
|
"version" : "9.5.0-20260326015913+0000",
|
||||||
"buildTime" : "20260325015243+0000",
|
"buildTime" : "20260326015913+0000",
|
||||||
"commitId" : "627839c6a3532eeab60306fd7b577d6f1c866ece",
|
"commitId" : "b62b56136fe3f28a01c3e35f77694c3d5af75916",
|
||||||
"current" : false,
|
"current" : false,
|
||||||
"snapshot" : true,
|
"snapshot" : true,
|
||||||
"nightly" : false,
|
"nightly" : false,
|
||||||
@@ -10,15 +10,15 @@
|
|||||||
"rcFor" : "",
|
"rcFor" : "",
|
||||||
"milestoneFor" : "",
|
"milestoneFor" : "",
|
||||||
"broken" : false,
|
"broken" : false,
|
||||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip",
|
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-bin.zip",
|
||||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip.sha256",
|
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-bin.zip.sha256",
|
||||||
"checksum" : "f031a868b2d9707fe07c78ad0888d4be5d6bb87e3f8a118ffbf3b0b497c32922",
|
"checksum" : "ace6a98f3a565a82cd108c6a115f64837cd5bb8e95d563c6e2dfb884ea3a8fe5",
|
||||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-wrapper.jar.sha256",
|
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260326015913+0000-wrapper.jar.sha256",
|
||||||
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
|
||||||
}, {
|
}, {
|
||||||
"version" : "9.6.0-20260325005438+0000",
|
"version" : "9.6.0-20260326003843+0000",
|
||||||
"buildTime" : "20260325005438+0000",
|
"buildTime" : "20260326003843+0000",
|
||||||
"commitId" : "4a2f60ed3e5db8c8eadc899d49da7c6abf7140ee",
|
"commitId" : "f6b5714b236ea05298517d966a339045da81a5ee",
|
||||||
"current" : false,
|
"current" : false,
|
||||||
"snapshot" : true,
|
"snapshot" : true,
|
||||||
"nightly" : true,
|
"nightly" : true,
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
"rcFor" : "",
|
"rcFor" : "",
|
||||||
"milestoneFor" : "",
|
"milestoneFor" : "",
|
||||||
"broken" : false,
|
"broken" : false,
|
||||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip",
|
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-bin.zip",
|
||||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip.sha256",
|
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-bin.zip.sha256",
|
||||||
"checksum" : "1419ec3a9f2e924772188c709cd681d073c7c821dbd82beb9a6e8f6b78f7723b",
|
"checksum" : "9d70bda347d4cdbc4fc8ce8550d53ce5d1b2add847f4720e8543ff6c74c322b8",
|
||||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-wrapper.jar.sha256",
|
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260326003843+0000-wrapper.jar.sha256",
|
||||||
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
||||||
}, {
|
}, {
|
||||||
"version" : "9.4.1",
|
"version" : "9.4.1",
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -2,14 +2,16 @@
|
|||||||
<typeInfoHistroy>
|
<typeInfoHistroy>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockService.java[TimeLockService" modifiers="1" timestamp="1774441060175"/>
|
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockService.java[TTLockService" modifiers="1" timestamp="1774375173709"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockService.java[TTLockService" modifiers="1" timestamp="1774375173709"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.user{UserRepository.java[UserRepository" modifiers="513" timestamp="1774016609131"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.user{UserRepository.java[UserRepository" modifiers="513" timestamp="1774016609131"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockUserConfigEntity.java[TTLockUserConfigEntity" modifiers="1" timestamp="1774425822887"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockUserConfigEntity.java[TTLockUserConfigEntity" modifiers="1" timestamp="1774425822887"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.keyholder{KeyholderNotificationEntity.java[KeyholderNotificationEntity" modifiers="1" timestamp="1774386563354"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.keyholder{KeyholderNotificationEntity.java[KeyholderNotificationEntity" modifiers="1" timestamp="1774386563354"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockCallback.java[TTLockCallback" modifiers="1" timestamp="1774387007874"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockCallback.java[TTLockCallback" modifiers="1" timestamp="1774387007874"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774422277518"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774508936868"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774171624571"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774171624571"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774388746804"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774511010754"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockEntity.java[BaseLockEntity" modifiers="1" timestamp="1774369125472"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.user{UserController.java[UserController" modifiers="1" timestamp="1774425860838"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockEntity.java[BaseLockEntity" modifiers="1" timestamp="1774476260239"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockService.java[TimeLockService" modifiers="1" timestamp="1774510681823"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockService.java[BaseLockService" modifiers="1025" timestamp="1774508825104"/>
|
||||||
</typeInfoHistroy>
|
</typeInfoHistroy>
|
||||||
|
|||||||
@@ -28,4 +28,5 @@
|
|||||||
<fullyQualifiedTypeName name="java.util.Optional"/>
|
<fullyQualifiedTypeName name="java.util.Optional"/>
|
||||||
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository"/>
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository"/>
|
||||||
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockCallback"/>
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockCallback"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.CardLockService"/>
|
||||||
</qualifiedTypeNameHistroy>
|
</qualifiedTypeNameHistroy>
|
||||||
|
|||||||
@@ -9,3 +9,7 @@
|
|||||||
2026-03-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
2026-03-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||||
2026-03-25 07:26:14,133 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
2026-03-25 07:26:14,133 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||||
2026-03-25 21:32:47,427 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
2026-03-25 21:32:47,427 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||||
|
2026-03-26 07:50:17,235 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||||
|
2026-03-26 10:32:24,614 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||||
|
2026-03-26 11:31:40,355 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||||
|
2026-03-26 16:50:11,098 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#Wed Mar 25 21:32:43 CET 2026
|
#Thu Mar 26 16:50:07 CET 2026
|
||||||
org.eclipse.core.runtime=2
|
org.eclipse.core.runtime=2
|
||||||
org.eclipse.platform=4.39.0.v20260226-0420
|
org.eclipse.platform=4.39.0.v20260226-0420
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/admin.html").authenticated()
|
.requestMatchers("/admin.html").authenticated()
|
||||||
.requestMatchers("/communityvotes.html").authenticated()
|
.requestMatchers("/communityvotes.html").authenticated()
|
||||||
.requestMatchers("/keyholder.html").authenticated()
|
.requestMatchers("/keyholder.html").authenticated()
|
||||||
|
.requestMatchers("/keyholder-finden.html").authenticated()
|
||||||
.requestMatchers("/meine-locks.html").authenticated()
|
.requestMatchers("/meine-locks.html").authenticated()
|
||||||
|
.requestMatchers("/entdecken-vorlagen.html").authenticated()
|
||||||
.requestMatchers("/unlock-history.html").authenticated()
|
.requestMatchers("/unlock-history.html").authenticated()
|
||||||
.requestMatchers("/einladungen.html").authenticated()
|
.requestMatchers("/einladungen.html").authenticated()
|
||||||
.requestMatchers("/joinlock.html").authenticated()
|
.requestMatchers("/joinlock.html").authenticated()
|
||||||
|
|||||||
@@ -47,16 +47,14 @@ public enum CardEnum {
|
|||||||
CUM {
|
CUM {
|
||||||
@Override
|
@Override
|
||||||
public Card get() {
|
public Card get() {
|
||||||
// TODO Auto-generated method stub
|
return new CumCard();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
CUM_IN_CAGE {
|
CUM_IN_CAGE {
|
||||||
@Override
|
@Override
|
||||||
public Card get() {
|
public Card get() {
|
||||||
// TODO Auto-generated method stub
|
return new CumInCageCard();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ public class CardLockController {
|
|||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
var lockee = lockeeOpt.get();
|
var lockee = lockeeOpt.get();
|
||||||
|
|
||||||
|
if (cardLockServiceFactory.hasActiveLock(req.lockeeUserId()))
|
||||||
|
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
CardLockEntity lock = new CardLockEntity();
|
CardLockEntity lock = new CardLockEntity();
|
||||||
lock.setName(req.name());
|
lock.setName(req.name());
|
||||||
@@ -194,7 +197,7 @@ public class CardLockController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Self-lockee path (existing behavior)
|
// Self-lockee path (existing behavior)
|
||||||
if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
|
if (cardLockServiceFactory.hasActiveLock(myId))
|
||||||
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||||
|
|
||||||
LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
|
LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
|
||||||
@@ -346,7 +349,7 @@ public class CardLockController {
|
|||||||
if (!l.getLockee().equals(myId))
|
if (!l.getLockee().equals(myId))
|
||||||
return ResponseEntity.status(403).build();
|
return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
String code = cardLockServiceFactory.create(l).endHygieneOpening();
|
String code = cardLockServiceFactory.create(l).endTempOpening();
|
||||||
return ResponseEntity.ok(Map.of("newUnlockCode", code));
|
return ResponseEntity.ok(Map.of("newUnlockCode", code));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,11 +424,11 @@ public class CardLockController {
|
|||||||
var meOpt = userRepository.findByEmail(principal.getName());
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
if (meOpt.isEmpty())
|
if (meOpt.isEmpty())
|
||||||
return ResponseEntity.status(401).build();
|
return ResponseEntity.status(401).build();
|
||||||
var locks = cardlockRepository.findByLockee(meOpt.get().getUserId());
|
UUID myId = meOpt.get().getUserId();
|
||||||
var active = locks.stream().filter(l -> l.getUnlockTime() == null).findFirst();
|
var activeLockId = cardLockServiceFactory.findActiveLockId(myId);
|
||||||
if (active.isEmpty())
|
if (activeLockId.isEmpty())
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
return ResponseEntity.ok(Map.of("lockId", active.get().getLockId().toString()));
|
return ResponseEntity.ok(Map.of("lockId", activeLockId.get().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/cardlock/{lockId}")
|
@GetMapping("/cardlock/{lockId}")
|
||||||
@@ -477,6 +480,11 @@ public class CardLockController {
|
|||||||
result.put("hygieneOpeningDue", hygieneOpeningDue);
|
result.put("hygieneOpeningDue", hygieneOpeningDue);
|
||||||
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
|
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
|
||||||
result.put("hygieneOpeningActive", l.getTempOpeningTime() != null && TempOpeningReason.HYGIENE == l.getTempOpeningReason());
|
result.put("hygieneOpeningActive", l.getTempOpeningTime() != null && TempOpeningReason.HYGIENE == l.getTempOpeningReason());
|
||||||
|
boolean tempOpeningActive = l.getTempOpeningTime() != null && TempOpeningReason.HYGIENE != l.getTempOpeningReason();
|
||||||
|
result.put("tempOpeningActive", tempOpeningActive);
|
||||||
|
if (tempOpeningActive) {
|
||||||
|
result.put("tempOpeningUnlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
||||||
|
}
|
||||||
result.put("hygieneOpeningStarted",
|
result.put("hygieneOpeningStarted",
|
||||||
l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
|
l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
|
||||||
result.put("hygieneDurationMinutes",
|
result.put("hygieneDurationMinutes",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
||||||
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
||||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||||
@@ -99,11 +98,13 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applyHygieneOvertime(Long overtime) {
|
protected void applyHygieneOvertime(Long overtime) {
|
||||||
|
LOGGER.debug("Apply {} Minutes Overtime");
|
||||||
if (lock.getFrozenUntil() != null) {
|
if (lock.getFrozenUntil() != null) {
|
||||||
lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4));
|
lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4));
|
||||||
} else {
|
} else {
|
||||||
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4));
|
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4));
|
||||||
}
|
}
|
||||||
|
LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Card drawing ──────────────────────────────────────────────────────────
|
// ── Card drawing ──────────────────────────────────────────────────────────
|
||||||
@@ -251,31 +252,6 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
return lock.getUnlockCode();
|
return lock.getUnlockCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String endCumming() {
|
|
||||||
Long overtime = calcOvertime();
|
|
||||||
if (overtime != null) {
|
|
||||||
if (lock.getKeyholder() == null) {
|
|
||||||
applyHygieneOvertime(overtime);
|
|
||||||
} else {
|
|
||||||
reportKeyholder(overtime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lock.setTempOpeningDuration(null);
|
|
||||||
lock.setTempOpeningTime(null);
|
|
||||||
lock.setTempOpeningReason(null);
|
|
||||||
|
|
||||||
if (lockControl != null
|
|
||||||
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
|
|
||||||
lockControl.lock();
|
|
||||||
cardLockRepository.save(lock);
|
|
||||||
return lock.getUnlockCode() != null ? lock.getUnlockCode() : "";
|
|
||||||
}
|
|
||||||
var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5);
|
|
||||||
lock.setUnlockCode(code);
|
|
||||||
cardLockRepository.save(lock);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Assigned task penalty ─────────────────────────────────────────────────
|
// ── Assigned task penalty ─────────────────────────────────────────────────
|
||||||
|
|
||||||
public void applyAssignedTaskPenalty(AssignedTaskEntity task) {
|
public void applyAssignedTaskPenalty(AssignedTaskEntity task) {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package de.oaa.xxx.games.chastity.cardlock;
|
package de.oaa.xxx.games.chastity.cardlock;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||||
@@ -35,11 +40,14 @@ public class CardLockServiceFactory {
|
|||||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||||
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||||
private final LockControlFactory lockControlFactory;
|
private final LockControlFactory lockControlFactory;
|
||||||
|
private final CardlockRepository cardlockRepository;
|
||||||
|
private final TimeLockRepository timeLockRepository;
|
||||||
|
|
||||||
public CardLockServiceFactory(
|
public CardLockServiceFactory(
|
||||||
CommunityVerificationRepository communityVerificationRepository,
|
CommunityVerificationRepository communityVerificationRepository,
|
||||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||||
CardLockRepository cardLockRepository,
|
CardLockRepository cardLockRepository,
|
||||||
|
CardlockRepository cardlockRepository,
|
||||||
GameHistoryRepository gameHistoryRepository,
|
GameHistoryRepository gameHistoryRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||||
@@ -48,8 +56,10 @@ public class CardLockServiceFactory {
|
|||||||
SystemMessageService systemMessageService,
|
SystemMessageService systemMessageService,
|
||||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||||
CommunityTaskVoteRepository communityTaskVoteRepository,
|
CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||||
LockControlFactory lockControlFactory) {
|
LockControlFactory lockControlFactory,
|
||||||
|
TimeLockRepository timeLockRepository) {
|
||||||
this.cardLockRepository = cardLockRepository;
|
this.cardLockRepository = cardLockRepository;
|
||||||
|
this.cardlockRepository = cardlockRepository;
|
||||||
this.communityVerificationRepository = communityVerificationRepository;
|
this.communityVerificationRepository = communityVerificationRepository;
|
||||||
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
|
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
|
||||||
this.gameHistoryRepository = gameHistoryRepository;
|
this.gameHistoryRepository = gameHistoryRepository;
|
||||||
@@ -61,6 +71,19 @@ public class CardLockServiceFactory {
|
|||||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||||
this.lockControlFactory = lockControlFactory;
|
this.lockControlFactory = lockControlFactory;
|
||||||
|
this.timeLockRepository = timeLockRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasActiveLock(UUID lockeeId) {
|
||||||
|
return BaseLockService.hasActiveLock(lockeeId, cardlockRepository, timeLockRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UUID> findActiveLockId(UUID lockeeId) {
|
||||||
|
var cardLock = cardlockRepository.findByLockee(lockeeId).stream()
|
||||||
|
.filter(l -> l.getUnlockTime() == null).findFirst();
|
||||||
|
if (cardLock.isPresent()) return Optional.of(cardLock.get().getLockId());
|
||||||
|
return timeLockRepository.findFirstByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId)
|
||||||
|
.map(l -> l.getLockId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import java.util.stream.Collectors;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity;
|
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
|
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
|
||||||
@@ -91,6 +93,18 @@ public abstract class BaseLockService {
|
|||||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Lockee-Prüfung ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob der Anwender bereits ein aktives Lock (CardLock oder TimeLock) als Lockee hat.
|
||||||
|
* Ein Lock gilt als aktiv wenn startTime gesetzt und unlockTime null ist.
|
||||||
|
*/
|
||||||
|
public static boolean hasActiveLock(UUID lockeeId, CardlockRepository cardlockRepo,
|
||||||
|
TimeLockRepository timelockRepo) {
|
||||||
|
return cardlockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId)
|
||||||
|
|| timelockRepo.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeId);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Gemeinsame Hilfsmethoden ──────────────────────────────────────────────
|
// ── Gemeinsame Hilfsmethoden ──────────────────────────────────────────────
|
||||||
|
|
||||||
protected Long calcOvertime() {
|
protected Long calcOvertime() {
|
||||||
@@ -192,10 +206,10 @@ public abstract class BaseLockService {
|
|||||||
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
|
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String endHygieneOpening() {
|
public String endTempOpening() {
|
||||||
BaseLockEntity lock = getLock();
|
var lock = getLock();
|
||||||
LocalDateTime now = LocalDateTime.now();
|
var now = LocalDateTime.now();
|
||||||
Long overtime = calcOvertime();
|
var overtime = calcOvertime();
|
||||||
if (overtime != null) {
|
if (overtime != null) {
|
||||||
if (lock.getKeyholder() != null) {
|
if (lock.getKeyholder() != null) {
|
||||||
reportKeyholder(overtime);
|
reportKeyholder(overtime);
|
||||||
@@ -203,19 +217,19 @@ public abstract class BaseLockService {
|
|||||||
applyHygieneOvertime(overtime);
|
applyHygieneOvertime(overtime);
|
||||||
}
|
}
|
||||||
afterHygieneClosing();
|
afterHygieneClosing();
|
||||||
lock.setLastHygineOpening(now);
|
if (TempOpeningReason.HYGIENE == lock.getTempOpeningReason()) {
|
||||||
|
lock.setLastHygineOpening(now);
|
||||||
|
}
|
||||||
|
lock.setTempOpeningReason(null);
|
||||||
lock.setTempOpeningDuration(null);
|
lock.setTempOpeningDuration(null);
|
||||||
lock.setTempOpeningTime(null);
|
lock.setTempOpeningTime(null);
|
||||||
if (lockControl != null
|
if (lockControl != null
|
||||||
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
|
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
|
||||||
// TTLock/Trust: lockControl.lock() setzt PIN am Gerät (oder tut nichts bei Trust).
|
lockControl.lock();
|
||||||
// Kein Software-Code notwendig.
|
|
||||||
lockControl.lock();
|
|
||||||
saveLock();
|
saveLock();
|
||||||
return lock.getUnlockCode() != null ? lock.getUnlockCode() : "";
|
return lock.getUnlockCode() != null ? lock.getUnlockCode() : "";
|
||||||
}
|
}
|
||||||
// UNLOCK_CODE (oder kein lockControl): neuen numerischen Code generieren
|
var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5);
|
||||||
String code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5);
|
|
||||||
lock.setUnlockCode(code);
|
lock.setUnlockCode(code);
|
||||||
saveLock();
|
saveLock();
|
||||||
return code;
|
return code;
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ public class BaseLockTemplateController {
|
|||||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||||
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
|
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
|
||||||
dto.put("requiresVerification", t.isRequiresVerification());
|
dto.put("requiresVerification", t.isRequiresVerification());
|
||||||
|
dto.put("published", t.isPublished());
|
||||||
|
dto.put("showAuthor", t.isShowAuthor());
|
||||||
return dto;
|
return dto;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ public class BaseLockTemplateEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private TaskMode taskMode = TaskMode.RANDOM;
|
private TaskMode taskMode = TaskMode.RANDOM;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean published = false;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean showAuthor = false;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private long subscriberCount = 0;
|
||||||
|
|
||||||
public TaskMode getTaskCardMode() {
|
public TaskMode getTaskCardMode() {
|
||||||
return taskMode != null ? taskMode : TaskMode.RANDOM;
|
return taskMode != null ? taskMode : TaskMode.RANDOM;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import org.springframework.data.domain.Page;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface BaseLockTemplateRepository extends JpaRepository<BaseLockTemplateEntity, UUID>{
|
public interface BaseLockTemplateRepository extends JpaRepository<BaseLockTemplateEntity, UUID> {
|
||||||
List<BaseLockTemplateEntity> findByOwner(UUID owner);
|
List<BaseLockTemplateEntity> findByOwner(UUID owner);
|
||||||
|
List<BaseLockTemplateEntity> findByOwnerAndPublishedTrue(UUID owner);
|
||||||
Page<BaseLockTemplateEntity> findByOwner(UUID owner, Pageable pageable);
|
Page<BaseLockTemplateEntity> findByOwner(UUID owner, Pageable pageable);
|
||||||
|
Page<BaseLockTemplateEntity> findByPublishedTrue(Pageable pageable);
|
||||||
|
Page<BaseLockTemplateEntity> findByPublishedTrueAndNameContainingIgnoreCase(String name, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.common;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateEntity;
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity;
|
||||||
|
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||||
|
import de.oaa.xxx.user.UserRepository;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/templates")
|
||||||
|
public class TemplateExploreController {
|
||||||
|
|
||||||
|
private final BaseLockTemplateRepository templateRepository;
|
||||||
|
private final TemplateSubscriptionRepository subscriptionRepository;
|
||||||
|
private final TimeLockTemplateRepository timeLockTemplateRepository;
|
||||||
|
private final CardlockTemplateRepository cardlockTemplateRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public TemplateExploreController(BaseLockTemplateRepository templateRepository,
|
||||||
|
TemplateSubscriptionRepository subscriptionRepository,
|
||||||
|
TimeLockTemplateRepository timeLockTemplateRepository,
|
||||||
|
CardlockTemplateRepository cardlockTemplateRepository,
|
||||||
|
UserRepository userRepository) {
|
||||||
|
this.templateRepository = templateRepository;
|
||||||
|
this.subscriptionRepository = subscriptionRepository;
|
||||||
|
this.timeLockTemplateRepository = timeLockTemplateRepository;
|
||||||
|
this.cardlockTemplateRepository = cardlockTemplateRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Öffentliche Vorlagen entdecken ────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/public")
|
||||||
|
public ResponseEntity<Map<String, Object>> getPublicTemplates(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "") String q,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
size = Math.min(size, 20);
|
||||||
|
var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "subscriberCount"));
|
||||||
|
|
||||||
|
Page<BaseLockTemplateEntity> pageResult = q.isBlank()
|
||||||
|
? templateRepository.findByPublishedTrue(pageable)
|
||||||
|
: templateRepository.findByPublishedTrueAndNameContainingIgnoreCase(q.trim(), pageable);
|
||||||
|
|
||||||
|
Set<UUID> subscribedIds = subscriptionRepository.findByUserId(myId)
|
||||||
|
.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<Map<String, Object>> content = pageResult.getContent().stream()
|
||||||
|
.map(t -> toPublicDto(t, myId, subscribedIds))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"content", content,
|
||||||
|
"page", pageResult.getNumber(),
|
||||||
|
"hasMore", !pageResult.isLast()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Einzelne Vorlage per ID (für Detail-Dialog) ──────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/{id}/public")
|
||||||
|
public ResponseEntity<Map<String, Object>> getTemplate(
|
||||||
|
@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(id);
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var t = tOpt.get();
|
||||||
|
|
||||||
|
// Sichtbar wenn: eigene Vorlage ODER veröffentlicht
|
||||||
|
if (!t.getOwner().equals(myId) && !t.isPublished()) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
Set<UUID> subscribedIds = subscriptionRepository.findByUserId(myId)
|
||||||
|
.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||||
|
return ResponseEntity.ok(toPublicDto(t, myId, subscribedIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eigene Vorlagen (für Auswahl-Dropdown) ───────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/mine")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getMyTemplates(Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = templateRepository.findByOwner(myId).stream()
|
||||||
|
.map(t -> toPublicDto(t, myId, Set.of()))
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Abonnierte Vorlagen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/subscribed")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getSubscribedTemplates(Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
List<TemplateSubscriptionEntity> subs = subscriptionRepository.findByUserId(myId);
|
||||||
|
Set<UUID> subscribedIds = subs.stream().map(TemplateSubscriptionEntity::getTemplateId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (var sub : subs) {
|
||||||
|
templateRepository.findById(sub.getTemplateId()).ifPresent(t -> {
|
||||||
|
if (t.isPublished()) {
|
||||||
|
result.add(toPublicDto(t, myId, subscribedIds));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Abonnieren ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@PostMapping("/{id}/subscribe")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> subscribe(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(id);
|
||||||
|
if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build();
|
||||||
|
if (tOpt.get().getOwner().equals(myId)) return ResponseEntity.status(409).build();
|
||||||
|
if (subscriptionRepository.findByUserIdAndTemplateId(myId, id).isPresent())
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
|
||||||
|
var sub = new TemplateSubscriptionEntity();
|
||||||
|
sub.setUserId(myId);
|
||||||
|
sub.setTemplateId(id);
|
||||||
|
sub.setSubscribedAt(LocalDateTime.now());
|
||||||
|
subscriptionRepository.save(sub);
|
||||||
|
|
||||||
|
var t = tOpt.get();
|
||||||
|
t.setSubscriberCount(t.getSubscriberCount() + 1);
|
||||||
|
templateRepository.save(t);
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Abo kündigen ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/subscribe")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> unsubscribe(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var subOpt = subscriptionRepository.findByUserIdAndTemplateId(myId, id);
|
||||||
|
if (subOpt.isEmpty()) return ResponseEntity.noContent().build();
|
||||||
|
|
||||||
|
subscriptionRepository.delete(subOpt.get());
|
||||||
|
|
||||||
|
templateRepository.findById(id).ifPresent(t -> {
|
||||||
|
t.setSubscriberCount(Math.max(0, t.getSubscriberCount() - 1));
|
||||||
|
templateRepository.save(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kopie erstellen (Fork) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@PostMapping("/{id}/fork")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> fork(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(id);
|
||||||
|
if (tOpt.isEmpty() || !tOpt.get().isPublished()) return ResponseEntity.notFound().build();
|
||||||
|
|
||||||
|
var source = tOpt.get();
|
||||||
|
String copyName = (source.getName() != null ? source.getName() : "Vorlage") + " (Kopie)";
|
||||||
|
|
||||||
|
UUID newId;
|
||||||
|
if (source instanceof TimeLockTemplateEntity tl) {
|
||||||
|
var copy = new TimeLockTemplateEntity();
|
||||||
|
copy.setOwner(myId);
|
||||||
|
copy.setName(copyName);
|
||||||
|
copy.setMinTimeInMinutes(tl.getMinTimeInMinutes());
|
||||||
|
copy.setMaxTimeInMinutes(tl.getMaxTimeInMinutes());
|
||||||
|
copy.setEndTimeVisible(tl.isEndTimeVisible());
|
||||||
|
copy.setHygineOpeningDurationMinutes(tl.getHygineOpeningDurationMinutes());
|
||||||
|
copy.setHygineOpeningEveryMinites(tl.getHygineOpeningEveryMinites());
|
||||||
|
copy.setTasks(tl.getTasks());
|
||||||
|
copy.setTaskEveryMinutes(tl.getTaskEveryMinutes());
|
||||||
|
copy.setMinTasksPerDay(tl.getMinTasksPerDay());
|
||||||
|
copy.setSpinningWheelEntries(tl.getSpinningWheelEntries());
|
||||||
|
copy.setSpinsEveryMinutes(tl.getSpinsEveryMinutes());
|
||||||
|
copy.setMinSpinsPerDay(tl.getMinSpinsPerDay());
|
||||||
|
copy.setRequiresVerification(tl.isRequiresVerification());
|
||||||
|
copy.setTaskMode(tl.getTaskMode() != null ? tl.getTaskMode() : TaskMode.RANDOM);
|
||||||
|
copy.setPenaltyType(tl.getPenaltyType());
|
||||||
|
copy.setPenaltyValue(tl.getPenaltyValue());
|
||||||
|
newId = timeLockTemplateRepository.save(copy).getTemplateId();
|
||||||
|
} else if (source instanceof CardlockTemplateEntity cl) {
|
||||||
|
var copy = new CardlockTemplateEntity();
|
||||||
|
copy.setOwner(myId);
|
||||||
|
copy.setName(copyName);
|
||||||
|
copy.setCardCountsMin(cl.getCardCountsMin());
|
||||||
|
copy.setCardCountsMax(cl.getCardCountsMax());
|
||||||
|
copy.setPickEveryMinute(cl.getPickEveryMinute());
|
||||||
|
copy.setAccumulatePicks(cl.isAccumulatePicks());
|
||||||
|
copy.setShowRemainingCards(cl.isShowRemainingCards());
|
||||||
|
copy.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes());
|
||||||
|
copy.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites());
|
||||||
|
copy.setTasks(cl.getTasks());
|
||||||
|
copy.setRequiresVerification(cl.isRequiresVerification());
|
||||||
|
copy.setTaskMode(cl.getTaskMode() != null ? cl.getTaskMode() : TaskMode.RANDOM);
|
||||||
|
newId = cardlockTemplateRepository.save(copy).getTemplateId();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.status(500).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("templateId", newId.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Veröffentlichen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@PatchMapping("/{id}/publish")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> publish(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(id);
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var t = tOpt.get();
|
||||||
|
if (!t.getOwner().equals(me.getUserId())) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
t.setPublished(true);
|
||||||
|
t.setShowAuthor(me.isProfilBeiVeroeffentlichungenSichtbar());
|
||||||
|
templateRepository.save(t);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Veröffentlichung entfernen ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/publish")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> unpublish(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(id);
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var t = tOpt.get();
|
||||||
|
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
t.setPublished(false);
|
||||||
|
t.setShowAuthor(false);
|
||||||
|
t.setSubscriberCount(0);
|
||||||
|
templateRepository.save(t);
|
||||||
|
subscriptionRepository.deleteByTemplateId(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DTO Helper ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Map<String, Object> toPublicDto(BaseLockTemplateEntity t, UUID myId, Set<UUID> subscribedIds) {
|
||||||
|
boolean isOwn = t.getOwner().equals(myId);
|
||||||
|
boolean isSubscribed = subscribedIds.contains(t.getTemplateId());
|
||||||
|
|
||||||
|
String authorName = null;
|
||||||
|
String authorProfilePicture = null;
|
||||||
|
if (t.isShowAuthor()) {
|
||||||
|
var authorOpt = userRepository.findById(t.getOwner());
|
||||||
|
authorName = authorOpt.map(u -> u.getName()).orElse(null);
|
||||||
|
authorProfilePicture = authorOpt.map(u -> u.getProfilePicture()).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> dto = new LinkedHashMap<>();
|
||||||
|
dto.put("templateId", t.getTemplateId().toString());
|
||||||
|
dto.put("lockType", t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK");
|
||||||
|
dto.put("name", t.getName() != null ? t.getName() : "");
|
||||||
|
dto.put("subscriberCount", t.getSubscriberCount());
|
||||||
|
dto.put("authorName", authorName);
|
||||||
|
dto.put("authorProfilePicture", authorProfilePicture);
|
||||||
|
dto.put("isOwnTemplate", isOwn);
|
||||||
|
dto.put("isSubscribed", isSubscribed);
|
||||||
|
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
|
||||||
|
dto.put("requiresVerification", t.isRequiresVerification());
|
||||||
|
dto.put("hygieneEnabled", t.getHygineOpeningEveryMinites() != null);
|
||||||
|
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||||
|
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||||
|
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
|
||||||
|
dto.put("taskMode", t.getTaskMode() != null ? t.getTaskMode().name() : "RANDOM");
|
||||||
|
|
||||||
|
if (t instanceof TimeLockTemplateEntity tl) {
|
||||||
|
dto.put("minTimeInMinutes", tl.getMinTimeInMinutes());
|
||||||
|
dto.put("maxTimeInMinutes", tl.getMaxTimeInMinutes());
|
||||||
|
dto.put("endTimeVisible", tl.isEndTimeVisible());
|
||||||
|
dto.put("taskEveryMinutes", tl.getTaskEveryMinutes());
|
||||||
|
dto.put("minTasksPerDay", tl.getMinTasksPerDay());
|
||||||
|
dto.put("spinningWheelEntries", tl.getSpinningWheelEntries() != null ? tl.getSpinningWheelEntries() : List.of());
|
||||||
|
dto.put("spinsEveryMinutes", tl.getSpinsEveryMinutes());
|
||||||
|
dto.put("minSpinsPerDay", tl.getMinSpinsPerDay());
|
||||||
|
dto.put("penaltyType", tl.getPenaltyType() != null ? tl.getPenaltyType().name() : null);
|
||||||
|
dto.put("penaltyValue", tl.getPenaltyValue());
|
||||||
|
} else if (t instanceof CardlockTemplateEntity cl) {
|
||||||
|
dto.put("cardCountsMin", cl.getCardCountsMin() != null ? cl.getCardCountsMin() : Map.of());
|
||||||
|
dto.put("cardCountsMax", cl.getCardCountsMax() != null ? cl.getCardCountsMax() : Map.of());
|
||||||
|
dto.put("pickEveryMinute", cl.getPickEveryMinute());
|
||||||
|
dto.put("accumulatePicks", cl.isAccumulatePicks());
|
||||||
|
dto.put("showRemainingCards",cl.isShowRemainingCards());
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.common;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "template_subscription")
|
||||||
|
public class TemplateSubscriptionEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID templateId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime subscribedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.common;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
public interface TemplateSubscriptionRepository extends JpaRepository<TemplateSubscriptionEntity, UUID> {
|
||||||
|
Optional<TemplateSubscriptionEntity> findByUserIdAndTemplateId(UUID userId, UUID templateId);
|
||||||
|
List<TemplateSubscriptionEntity> findByUserId(UUID userId);
|
||||||
|
long countByTemplateId(UUID templateId);
|
||||||
|
@Transactional
|
||||||
|
void deleteByTemplateId(UUID templateId);
|
||||||
|
@Transactional
|
||||||
|
void deleteByUserIdAndTemplateId(UUID userId, UUID templateId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.keyholder;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.*;
|
||||||
|
import de.oaa.xxx.games.chastity.common.*;
|
||||||
|
import de.oaa.xxx.games.chastity.lockcontroll.LockControllType;
|
||||||
|
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||||
|
import de.oaa.xxx.games.chastity.timelock.*;
|
||||||
|
import de.oaa.xxx.social.SystemMessageService;
|
||||||
|
import de.oaa.xxx.social.entity.MessageCause;
|
||||||
|
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||||
|
import de.oaa.xxx.user.Geschlecht;
|
||||||
|
import de.oaa.xxx.user.UserRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/keyholder-offers")
|
||||||
|
public class KeyholderOfferController {
|
||||||
|
|
||||||
|
private final KeyholderOfferRepository offerRepository;
|
||||||
|
private final BaseLockTemplateRepository templateRepository;
|
||||||
|
private final TemplateSubscriptionRepository subscriptionRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final CardlockRepository cardlockRepository;
|
||||||
|
private final TimeLockRepository timeLockRepository;
|
||||||
|
private final CardLockServiceFactory cardLockServiceFactory;
|
||||||
|
private final TimeLockServiceFactory timeLockServiceFactory;
|
||||||
|
private final KeyholderInvitationRepository invitationRepository;
|
||||||
|
private final SystemMessageService systemMessageService;
|
||||||
|
private final SubscriptionLimitService subscriptionLimitService;
|
||||||
|
|
||||||
|
public KeyholderOfferController(
|
||||||
|
KeyholderOfferRepository offerRepository,
|
||||||
|
BaseLockTemplateRepository templateRepository,
|
||||||
|
TemplateSubscriptionRepository subscriptionRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
CardlockRepository cardlockRepository,
|
||||||
|
TimeLockRepository timeLockRepository,
|
||||||
|
CardLockServiceFactory cardLockServiceFactory,
|
||||||
|
TimeLockServiceFactory timeLockServiceFactory,
|
||||||
|
KeyholderInvitationRepository invitationRepository,
|
||||||
|
SystemMessageService systemMessageService,
|
||||||
|
SubscriptionLimitService subscriptionLimitService) {
|
||||||
|
this.offerRepository = offerRepository;
|
||||||
|
this.templateRepository = templateRepository;
|
||||||
|
this.subscriptionRepository = subscriptionRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.cardlockRepository = cardlockRepository;
|
||||||
|
this.timeLockRepository = timeLockRepository;
|
||||||
|
this.cardLockServiceFactory = cardLockServiceFactory;
|
||||||
|
this.timeLockServiceFactory = timeLockServiceFactory;
|
||||||
|
this.invitationRepository = invitationRepository;
|
||||||
|
this.systemMessageService = systemMessageService;
|
||||||
|
this.subscriptionLimitService = subscriptionLimitService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eigene Angebote ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/mine")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getMyOffers(Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = offerRepository.findByOffererId(myId).stream()
|
||||||
|
.map(this::toDto)
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
record CreateOfferRequest(UUID templateId, List<String> targetGenders, boolean directStart) {}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> createOffer(
|
||||||
|
@RequestBody CreateOfferRequest req, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
UUID myId = me.getUserId();
|
||||||
|
|
||||||
|
// Limit prüfen
|
||||||
|
long existing = offerRepository.countByOffererId(myId);
|
||||||
|
if (existing >= subscriptionLimitService.maxKeyholderOffers(myId)) {
|
||||||
|
return ResponseEntity.status(403).body(Map.of("error", "offer_limit_reached"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template muss dem User gehören oder abonniert sein
|
||||||
|
var tOpt = templateRepository.findById(req.templateId());
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
||||||
|
var t = tOpt.get();
|
||||||
|
|
||||||
|
boolean isOwn = t.getOwner().equals(myId);
|
||||||
|
boolean isSubscribed = subscriptionRepository.findByUserIdAndTemplateId(myId, req.templateId()).isPresent();
|
||||||
|
if (!isOwn && !isSubscribed) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
String genders = req.targetGenders() != null
|
||||||
|
? String.join(",", req.targetGenders())
|
||||||
|
: "";
|
||||||
|
|
||||||
|
String templateType = t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK";
|
||||||
|
|
||||||
|
KeyholderOfferEntity offer = new KeyholderOfferEntity();
|
||||||
|
offer.setOffererId(myId);
|
||||||
|
offer.setTemplateId(req.templateId());
|
||||||
|
offer.setTemplateName(t.getName() != null ? t.getName() : "Unbenannt");
|
||||||
|
offer.setTemplateType(templateType);
|
||||||
|
offer.setTargetGenders(genders);
|
||||||
|
offer.setDirectStart(req.directStart());
|
||||||
|
offer.setAcceptanceCount(0);
|
||||||
|
offer.setCreatedAt(LocalDateTime.now());
|
||||||
|
offerRepository.save(offer);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(toDto(offer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{id}")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> updateOffer(
|
||||||
|
@PathVariable UUID id,
|
||||||
|
@RequestBody CreateOfferRequest req,
|
||||||
|
Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var offerOpt = offerRepository.findById(id);
|
||||||
|
if (offerOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var offer = offerOpt.get();
|
||||||
|
if (!offer.getOffererId().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
// Template darf geändert werden – muss dem User gehören oder abonniert sein
|
||||||
|
var tOpt = templateRepository.findById(req.templateId());
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
||||||
|
var t = tOpt.get();
|
||||||
|
boolean isOwn = t.getOwner().equals(myId);
|
||||||
|
boolean isSubscribed = subscriptionRepository.findByUserIdAndTemplateId(myId, req.templateId()).isPresent();
|
||||||
|
if (!isOwn && !isSubscribed) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
String templateType = t instanceof TimeLockTemplateEntity ? "TIMELOCK" : "CARDLOCK";
|
||||||
|
String genders = req.targetGenders() != null ? String.join(",", req.targetGenders()) : "";
|
||||||
|
|
||||||
|
offer.setTemplateId(req.templateId());
|
||||||
|
offer.setTemplateName(t.getName() != null ? t.getName() : "Unbenannt");
|
||||||
|
offer.setTemplateType(templateType);
|
||||||
|
offer.setTargetGenders(genders);
|
||||||
|
offer.setDirectStart(req.directStart());
|
||||||
|
offerRepository.save(offer);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(toDto(offer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> deleteOffer(@PathVariable UUID id, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var offerOpt = offerRepository.findById(id);
|
||||||
|
if (offerOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
if (!offerOpt.get().getOffererId().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
offerRepository.delete(offerOpt.get());
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Angebote eines bestimmten Nutzers (für Profilseite) ──────────────────
|
||||||
|
|
||||||
|
@GetMapping("/user/{userId}")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getOffersForUser(
|
||||||
|
@PathVariable UUID userId, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = offerRepository.findByOffererId(userId).stream()
|
||||||
|
.map(o -> toPublicDto(o, myId))
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Öffentliche Angebotsübersicht ─────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/public")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getPublicOffers(Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
|
||||||
|
String userGender = me.getGeschlecht() != null ? me.getGeschlecht().name() : null;
|
||||||
|
|
||||||
|
List<Map<String, Object>> result = offerRepository.findAllByOrderByAcceptanceCountDesc().stream()
|
||||||
|
.filter(o -> matchesGender(o, userGender))
|
||||||
|
.map(o -> toPublicDto(o, me.getUserId()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesGender(KeyholderOfferEntity o, String userGender) {
|
||||||
|
String tg = o.getTargetGenders();
|
||||||
|
if (tg == null || tg.isBlank()) return true; // alle Geschlechter
|
||||||
|
if (userGender == null) return false; // User ohne Geschlecht: nur unrestricted
|
||||||
|
return Arrays.asList(tg.split(",")).contains(userGender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Angebot annehmen (Lock erstellen) ─────────────────────────────────────
|
||||||
|
|
||||||
|
record JoinOfferRequest(LockControllType controllType, Integer unlockCodeLength) {}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/join")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Map<String, Object>> joinOffer(
|
||||||
|
@PathVariable UUID id,
|
||||||
|
@RequestBody JoinOfferRequest req,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
UUID myId = me.getUserId();
|
||||||
|
|
||||||
|
var offerOpt = offerRepository.findById(id);
|
||||||
|
if (offerOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var offer = offerOpt.get();
|
||||||
|
|
||||||
|
if (offer.getOffererId().equals(myId))
|
||||||
|
return ResponseEntity.status(409).body(Map.of("error", "own_offer"));
|
||||||
|
|
||||||
|
// Aktives Lock prüfen
|
||||||
|
if (cardLockServiceFactory.hasActiveLock(myId))
|
||||||
|
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||||
|
|
||||||
|
var tOpt = templateRepository.findById(offer.getTemplateId());
|
||||||
|
if (tOpt.isEmpty()) return ResponseEntity.status(410).body(Map.of("error", "template_gone"));
|
||||||
|
var template = tOpt.get();
|
||||||
|
|
||||||
|
var offererOpt = userRepository.findById(offer.getOffererId());
|
||||||
|
if (offererOpt.isEmpty()) return ResponseEntity.status(410).body(Map.of("error", "offerer_gone"));
|
||||||
|
var offerer = offererOpt.get();
|
||||||
|
|
||||||
|
LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
|
||||||
|
int codeLen = (req.unlockCodeLength() != null && req.unlockCodeLength() >= 1) ? req.unlockCodeLength() : 5;
|
||||||
|
boolean directStart = offer.isDirectStart();
|
||||||
|
UUID keyholderIdIfDirect = directStart ? offer.getOffererId() : null;
|
||||||
|
|
||||||
|
UUID lockId;
|
||||||
|
String unlockCode = null;
|
||||||
|
|
||||||
|
if (template instanceof TimeLockTemplateEntity tl) {
|
||||||
|
TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings(
|
||||||
|
controllType, myId, keyholderIdIfDirect, false, codeLen);
|
||||||
|
TimeLockEntity lock = new TimeLockEntity();
|
||||||
|
timeLockServiceFactory.create(lock).init(tl, settings);
|
||||||
|
timeLockRepository.save(lock);
|
||||||
|
lockId = lock.getLockId();
|
||||||
|
unlockCode = lock.getUnlockCode();
|
||||||
|
|
||||||
|
} else if (template instanceof CardlockTemplateEntity cl) {
|
||||||
|
List<CardEnum> cards = buildCardList(cl);
|
||||||
|
if (cards.isEmpty()) return ResponseEntity.badRequest().body(Map.of("error", "empty_deck"));
|
||||||
|
|
||||||
|
CardLockEntity lock = new CardLockEntity();
|
||||||
|
lock.setName(template.getName());
|
||||||
|
lock.setLockee(myId);
|
||||||
|
lock.setKeyholder(keyholderIdIfDirect);
|
||||||
|
lock.setInitialCards(cards);
|
||||||
|
lock.setPickEveryMinute(cl.getPickEveryMinute() != null ? cl.getPickEveryMinute() : 60);
|
||||||
|
lock.setAccumulatePicks(cl.isAccumulatePicks());
|
||||||
|
lock.setShowRemainingCards(cl.isShowRemainingCards());
|
||||||
|
lock.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes());
|
||||||
|
lock.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites());
|
||||||
|
lock.setTasks(cl.getTasks() != null ? cl.getTasks() : List.of());
|
||||||
|
lock.setRequiresVerification(cl.isRequiresVerification());
|
||||||
|
lock.setTestLock(false);
|
||||||
|
lock.setTaskMode(cl.getTaskMode() != null ? cl.getTaskMode() : TaskMode.RANDOM);
|
||||||
|
lock.setUnlockCodeLength(codeLen);
|
||||||
|
lock.setControllType(controllType);
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
lock.setStartTime(now);
|
||||||
|
lock.setAvailableCards(new ArrayList<>(cards));
|
||||||
|
lock.setOpenPicks(0);
|
||||||
|
lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute()));
|
||||||
|
if (cl.getHygineOpeningEveryMinites() != null) {
|
||||||
|
lock.setLastHygineOpening(now);
|
||||||
|
}
|
||||||
|
cardlockRepository.save(lock);
|
||||||
|
|
||||||
|
CardLockService initService = cardLockServiceFactory.create(lock);
|
||||||
|
if (initService.getLockControl() != null) {
|
||||||
|
initService.getLockControl().lock();
|
||||||
|
} else {
|
||||||
|
lock.setUnlockCode(CodeCreator.createNumeric(codeLen));
|
||||||
|
cardlockRepository.save(lock);
|
||||||
|
}
|
||||||
|
lockId = lock.getLockId();
|
||||||
|
unlockCode = lock.getUnlockCode();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.status(500).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||||
|
boolean invitationSent = false;
|
||||||
|
if (!directStart) {
|
||||||
|
// Normaler Einladungsworkflow: Keyholder muss bestätigen
|
||||||
|
KeyholderInvitationEntity inv = new KeyholderInvitationEntity();
|
||||||
|
inv.setLockId(lockId);
|
||||||
|
inv.setKeyholderUserId(offer.getOffererId());
|
||||||
|
inv.setLockeeUserId(myId);
|
||||||
|
inv.setToken(UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
inv.setCreatedAt(LocalDateTime.now());
|
||||||
|
invitationRepository.save(inv);
|
||||||
|
|
||||||
|
systemMessageService.send(myId, offerer.getUserId(),
|
||||||
|
me.getName() + " möchte dein Keyholder-Angebot annehmen und lädt dich als Keyholder für „"
|
||||||
|
+ lockName + "\" ein.",
|
||||||
|
"/einladungen.html", MessageCause.INVITATION);
|
||||||
|
invitationSent = true;
|
||||||
|
} else {
|
||||||
|
// Direktstart: Keyholder wird direkt gesetzt, aber trotzdem benachrichtigen
|
||||||
|
systemMessageService.send(myId, offerer.getUserId(),
|
||||||
|
me.getName() + " hat dein Keyholder-Angebot angenommen und das Lock „"
|
||||||
|
+ lockName + "\" gestartet.",
|
||||||
|
"/keyholder.html", MessageCause.INVITATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annahmezähler erhöhen
|
||||||
|
offer.setAcceptanceCount(offer.getAcceptanceCount() + 1);
|
||||||
|
offerRepository.save(offer);
|
||||||
|
|
||||||
|
Map<String, Object> response = new LinkedHashMap<>();
|
||||||
|
response.put("lockId", lockId.toString());
|
||||||
|
response.put("invitationSent", invitationSent);
|
||||||
|
if (unlockCode != null) response.put("unlockCode", unlockCode);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<CardEnum> buildCardList(CardlockTemplateEntity cl) {
|
||||||
|
List<CardEnum> cards = new ArrayList<>();
|
||||||
|
Map<String, Integer> counts = cl.getCardCountsMax();
|
||||||
|
if (counts == null) return cards;
|
||||||
|
counts.forEach((type, count) -> {
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
try {
|
||||||
|
CardEnum card = CardEnum.valueOf(type);
|
||||||
|
for (int i = 0; i < count; i++) cards.add(card);
|
||||||
|
} catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toDto(KeyholderOfferEntity o) {
|
||||||
|
Map<String, Object> dto = new LinkedHashMap<>();
|
||||||
|
dto.put("id", o.getId().toString());
|
||||||
|
dto.put("templateId", o.getTemplateId().toString());
|
||||||
|
dto.put("templateName", o.getTemplateName());
|
||||||
|
dto.put("templateType", o.getTemplateType());
|
||||||
|
dto.put("targetGenders", o.getTargetGenders() != null ? Arrays.asList(o.getTargetGenders().split(",")).stream().filter(s -> !s.isBlank()).toList() : List.of());
|
||||||
|
dto.put("directStart", o.isDirectStart());
|
||||||
|
dto.put("acceptanceCount", o.getAcceptanceCount());
|
||||||
|
dto.put("createdAt", o.getCreatedAt().toString());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> toPublicDto(KeyholderOfferEntity o, UUID myId) {
|
||||||
|
Map<String, Object> dto = toDto(o);
|
||||||
|
dto.put("isOwn", o.getOffererId().equals(myId));
|
||||||
|
userRepository.findById(o.getOffererId()).ifPresent(u -> {
|
||||||
|
dto.put("offererName", u.getName());
|
||||||
|
dto.put("offererProfilePic", u.getProfilePicture());
|
||||||
|
});
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.keyholder;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "keyholder_offer")
|
||||||
|
public class KeyholderOfferEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
@Column
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID offererId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID templateId;
|
||||||
|
|
||||||
|
/** Denormalisiert für schnelle Anzeige */
|
||||||
|
@Column(length = 200)
|
||||||
|
private String templateName;
|
||||||
|
|
||||||
|
/** "CARDLOCK" oder "TIMELOCK" – denormalisiert */
|
||||||
|
@Column(length = 20)
|
||||||
|
private String templateType;
|
||||||
|
|
||||||
|
/** Kommagetrennte Geschlechter: z.B. "WEIBLICH,DIVERS" – leer = alle */
|
||||||
|
@Column(length = 100)
|
||||||
|
private String targetGenders;
|
||||||
|
|
||||||
|
/** true = Lock wird sofort gestartet; false = Keyholder muss erst bestätigen */
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean directStart;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int acceptanceCount = 0;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.oaa.xxx.games.chastity.keyholder;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface KeyholderOfferRepository extends JpaRepository<KeyholderOfferEntity, UUID> {
|
||||||
|
List<KeyholderOfferEntity> findByOffererId(UUID offererId);
|
||||||
|
long countByOffererId(UUID offererId);
|
||||||
|
List<KeyholderOfferEntity> findAllByOrderByAcceptanceCountDesc();
|
||||||
|
}
|
||||||
@@ -257,7 +257,7 @@ public class LockeeInvitationController {
|
|||||||
unlockCode = CodeCreator.createNumeric(codeLines);
|
unlockCode = CodeCreator.createNumeric(codeLines);
|
||||||
int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes());
|
int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes());
|
||||||
timeLock.setStartTime(now);
|
timeLock.setStartTime(now);
|
||||||
timeLock.setUnlockTime(now.plusMinutes(unlockMinutes));
|
timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes));
|
||||||
timeLock.setUnlockCode(unlockCode);
|
timeLock.setUnlockCode(unlockCode);
|
||||||
timeLock.setUnlockCodeLength(codeLines);
|
timeLock.setUnlockCodeLength(codeLines);
|
||||||
if (timeLock.getHygineOpeningEveryMinites() != null) {
|
if (timeLock.getHygineOpeningEveryMinites() != null) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -120,6 +121,9 @@ public class TimeLockController {
|
|||||||
if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
||||||
var lockee = lockeeOpt.get();
|
var lockee = lockeeOpt.get();
|
||||||
|
|
||||||
|
if (timeLockServiceFactory.hasActiveLock(req.lockeeUserId()))
|
||||||
|
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||||
|
|
||||||
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
|
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
|
||||||
lock.setStartTime(null);
|
lock.setStartTime(null);
|
||||||
timeLockRepository.save(lock);
|
timeLockRepository.save(lock);
|
||||||
@@ -144,7 +148,7 @@ public class TimeLockController {
|
|||||||
"lockeeInvitationSent", true));
|
"lockeeInvitationSent", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
|
if (timeLockServiceFactory.hasActiveLock(myId))
|
||||||
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||||
|
|
||||||
LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
|
LockControllType controllType = req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE;
|
||||||
@@ -223,7 +227,7 @@ public class TimeLockController {
|
|||||||
timeLockRepository.save(l);
|
timeLockRepository.save(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
|
boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
|
||||||
boolean isFrozen = l.getFrozenFrom() != null
|
boolean isFrozen = l.getFrozenFrom() != null
|
||||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||||
|
|
||||||
@@ -319,7 +323,7 @@ public class TimeLockController {
|
|||||||
|
|
||||||
// Only expose unlock time if end time is visible OR time is up
|
// Only expose unlock time if end time is visible OR time is up
|
||||||
if (l.isEndTimeVisible() || timeUp) {
|
if (l.isEndTimeVisible() || timeUp) {
|
||||||
result.put("unlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);
|
result.put("unlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null);
|
||||||
} else {
|
} else {
|
||||||
result.put("unlockTime", null);
|
result.put("unlockTime", null);
|
||||||
}
|
}
|
||||||
@@ -360,11 +364,14 @@ public class TimeLockController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.put("controllType", l.getControllType() != null ? l.getControllType().name() : "UNLOCK_CODE");
|
||||||
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
|
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
|
||||||
if (l.isKeyholderRequestedUnlock() || l.isTestLock() || timeUp) {
|
if (l.isKeyholderRequestedUnlock() || l.isTestLock() || timeUp) {
|
||||||
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
||||||
}
|
}
|
||||||
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
|
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
|
||||||
|
result.put("emergencyAutoUnlocked", l.isEmergencyAutoUnlocked());
|
||||||
|
result.put("actualUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);
|
||||||
|
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
@@ -382,7 +389,7 @@ public class TimeLockController {
|
|||||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
var l = lockOpt.get();
|
var l = lockOpt.get();
|
||||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
if (l.getUnlockTime() == null) return ResponseEntity.status(409).build(); // not started
|
if (l.getEstimatedUnlockTime() == null) return ResponseEntity.status(409).build(); // not started
|
||||||
if (l.getSpinningWheelEntries() == null || l.getSpinningWheelEntries().isEmpty())
|
if (l.getSpinningWheelEntries() == null || l.getSpinningWheelEntries().isEmpty())
|
||||||
return ResponseEntity.status(409).build();
|
return ResponseEntity.status(409).build();
|
||||||
|
|
||||||
@@ -414,7 +421,7 @@ public class TimeLockController {
|
|||||||
result.put("intVal", entry.getIntVal());
|
result.put("intVal", entry.getIntVal());
|
||||||
result.put("stringVal", entry.getStringVal());
|
result.put("stringVal", entry.getStringVal());
|
||||||
// Include updated lock time fields
|
// Include updated lock time fields
|
||||||
result.put("newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);
|
result.put("newUnlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null);
|
||||||
result.put("newFrozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
result.put("newFrozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||||
result.put("isFrozen", l.getFrozenFrom() != null);
|
result.put("isFrozen", l.getFrozenFrom() != null);
|
||||||
result.put("currentTask", l.getCurrentTask());
|
result.put("currentTask", l.getCurrentTask());
|
||||||
@@ -505,11 +512,11 @@ public class TimeLockController {
|
|||||||
return ResponseEntity.status(409).build();
|
return ResponseEntity.status(409).build();
|
||||||
|
|
||||||
TimeLockService service = timeLockServiceFactory.create(l);
|
TimeLockService service = timeLockServiceFactory.create(l);
|
||||||
String newCode = service.endHygieneOpening();
|
String newCode = service.endTempOpening();
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"newUnlockCode", newCode,
|
"newUnlockCode", newCode,
|
||||||
"newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : ""));
|
"newUnlockTime", l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Verifikation starten ─────────────────────────────────────────────────────
|
// ── Verifikation starten ─────────────────────────────────────────────────────
|
||||||
@@ -576,7 +583,7 @@ public class TimeLockController {
|
|||||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
|
boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
|
||||||
boolean isFrozen = l.getFrozenFrom() != null
|
boolean isFrozen = l.getFrozenFrom() != null
|
||||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||||
|
|
||||||
@@ -604,6 +611,30 @@ public class TimeLockController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tatsächliche Entsperrzeit setzen ─────────────────────────────────────────
|
||||||
|
|
||||||
|
@PatchMapping("/timelock/{lockId}/unlock-time")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<Void> setActualUnlockTime(@PathVariable UUID lockId, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
UUID myId = meOpt.get().getUserId();
|
||||||
|
|
||||||
|
var lockOpt = timeLockRepository.findById(lockId);
|
||||||
|
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var l = lockOpt.get();
|
||||||
|
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
|
||||||
|
if (!timeUp && !l.isTestLock() && !l.isKeyholderRequestedUnlock())
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
|
||||||
|
l.setUnlockTime(now);
|
||||||
|
timeLockRepository.save(l);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Keyholder-Ansicht ─────────────────────────────────────────────────────────
|
// ── Keyholder-Ansicht ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@GetMapping("/timelock/as-keyholder")
|
@GetMapping("/timelock/as-keyholder")
|
||||||
@@ -653,7 +684,7 @@ public class TimeLockController {
|
|||||||
var lockee = lockeeOpt.get();
|
var lockee = lockeeOpt.get();
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
|
boolean timeUp = l.getEstimatedUnlockTime() != null && l.getEstimatedUnlockTime().isBefore(now);
|
||||||
boolean isFrozen = l.getFrozenFrom() != null
|
boolean isFrozen = l.getFrozenFrom() != null
|
||||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||||
|
|
||||||
@@ -689,7 +720,7 @@ public class TimeLockController {
|
|||||||
result.put("lockeeProfilePic", lockee.getProfilePicture());
|
result.put("lockeeProfilePic", lockee.getProfilePicture());
|
||||||
result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null);
|
result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null);
|
||||||
result.put("unlockTime", (l.isEndTimeVisible() || timeUp)
|
result.put("unlockTime", (l.isEndTimeVisible() || timeUp)
|
||||||
? (l.getUnlockTime() != null ? l.getUnlockTime().toString() : null) : null);
|
? (l.getEstimatedUnlockTime() != null ? l.getEstimatedUnlockTime().toString() : null) : null);
|
||||||
result.put("timeUp", timeUp);
|
result.put("timeUp", timeUp);
|
||||||
result.put("isFrozen", isFrozen);
|
result.put("isFrozen", isFrozen);
|
||||||
result.put("frozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
result.put("frozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||||
@@ -721,6 +752,73 @@ public class TimeLockController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Einfrieren (Keyholder) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
record FreezeRequest(String frozenUntil) {}
|
||||||
|
|
||||||
|
@PostMapping("/timelock/as-keyholder/{lockId}/freeze")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<?> freezeTimeLock(@PathVariable UUID lockId,
|
||||||
|
@RequestBody FreezeRequest req, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
UUID myId = me.getUserId();
|
||||||
|
|
||||||
|
var lockOpt = timeLockRepository.findById(lockId);
|
||||||
|
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var l = lockOpt.get();
|
||||||
|
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
LocalDateTime until;
|
||||||
|
try {
|
||||||
|
until = LocalDateTime.parse(req.frozenUntil());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat."));
|
||||||
|
}
|
||||||
|
if (!until.isAfter(LocalDateTime.now()))
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen."));
|
||||||
|
|
||||||
|
l.setFrozenFrom(LocalDateTime.now());
|
||||||
|
l.setFrozenUntil(until);
|
||||||
|
timeLockRepository.save(l);
|
||||||
|
|
||||||
|
systemMessageService.send(myId, l.getLockee(),
|
||||||
|
me.getName() + " hat dein Lock bis "
|
||||||
|
+ until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + " "
|
||||||
|
+ until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
|
+ " Uhr eingefroren.",
|
||||||
|
"/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/timelock/as-keyholder/{lockId}/freeze")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<?> unfreezeTimeLock(@PathVariable UUID lockId, Principal principal) {
|
||||||
|
var meOpt = userRepository.findByEmail(principal.getName());
|
||||||
|
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||||
|
var me = meOpt.get();
|
||||||
|
UUID myId = me.getUserId();
|
||||||
|
|
||||||
|
var lockOpt = timeLockRepository.findById(lockId);
|
||||||
|
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
var l = lockOpt.get();
|
||||||
|
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
TimeLockService service = timeLockServiceFactory.create(l);
|
||||||
|
service.unfreeze();
|
||||||
|
l.setFrozenFrom(null);
|
||||||
|
l.setFrozenUntil(null);
|
||||||
|
timeLockRepository.save(l);
|
||||||
|
|
||||||
|
systemMessageService.send(myId, l.getLockee(),
|
||||||
|
me.getName() + " hat dein Lock entfroren.",
|
||||||
|
"/activetimelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Notfall-Entsperrung ───────────────────────────────────────────────────────
|
// ── Notfall-Entsperrung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@PostMapping("/timelock/{lockId}/emergency-unlock")
|
@PostMapping("/timelock/{lockId}/emergency-unlock")
|
||||||
@@ -800,4 +898,5 @@ public class TimeLockController {
|
|||||||
ImageIO.write(scaled, "jpeg", out);
|
ImageIO.write(scaled, "jpeg", out);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public class TimeLockEntity extends BaseLockEntity {
|
|||||||
@Column
|
@Column
|
||||||
private Integer penaltyValue;
|
private Integer penaltyValue;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private LocalDateTime estimatedUnlockTime;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime frozenFrom;
|
private LocalDateTime frozenFrom;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public interface TimeLockRepository extends JpaRepository<TimeLockEntity, UUID>
|
|||||||
|
|
||||||
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
|
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
|
||||||
|
|
||||||
|
java.util.Optional<TimeLockEntity> findFirstByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
|
||||||
|
|
||||||
List<TimeLockEntity> findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID keyholder);
|
List<TimeLockEntity> findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID keyholder);
|
||||||
|
|
||||||
@Modifying(clearAutomatically = true)
|
@Modifying(clearAutomatically = true)
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void applyHygieneOvertime(Long overtime) {
|
protected void applyHygieneOvertime(Long overtime) {
|
||||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4));
|
LOGGER.debug("Apply {} Minutes Overtime");
|
||||||
|
lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(overtime * 4));
|
||||||
|
LOGGER.debug("New estimated endtime {}", lock.getEstimatedUnlockTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hook overrides ────────────────────────────────────────────────────────
|
// ── Hook overrides ────────────────────────────────────────────────────────
|
||||||
@@ -135,7 +137,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes)
|
int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes)
|
||||||
? minMinutes + new Random().nextInt(maxMinutes - minMinutes)
|
? minMinutes + new Random().nextInt(maxMinutes - minMinutes)
|
||||||
: maxMinutes;
|
: maxMinutes;
|
||||||
lock.setUnlockTime(now.plusMinutes(unlockTimeMinutes));
|
lock.setEstimatedUnlockTime(now.plusMinutes(unlockTimeMinutes));
|
||||||
lock.setEndTimeVisible(template.isEndTimeVisible());
|
lock.setEndTimeVisible(template.isEndTimeVisible());
|
||||||
|
|
||||||
lock.setTasks(template.getTasks());
|
lock.setTasks(template.getTasks());
|
||||||
@@ -178,12 +180,12 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
|
|
||||||
public void addTime(Integer intVal) {
|
public void addTime(Integer intVal) {
|
||||||
LOGGER.debug("Lock addTime: %s minutes", intVal);
|
LOGGER.debug("Lock addTime: %s minutes", intVal);
|
||||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal));
|
lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(intVal));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeTime(Integer intVal) {
|
public void removeTime(Integer intVal) {
|
||||||
LOGGER.debug("Lock removeTime: %s minutes", intVal);
|
LOGGER.debug("Lock removeTime: %s minutes", intVal);
|
||||||
lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal));
|
lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().minusMinutes(intVal));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void freeze(Integer intVal) {
|
public void freeze(Integer intVal) {
|
||||||
@@ -202,7 +204,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now();
|
var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now();
|
||||||
var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime);
|
var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime);
|
||||||
LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff);
|
LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff);
|
||||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(diff));
|
lock.setEstimatedUnlockTime(lock.getEstimatedUnlockTime().plusMinutes(diff));
|
||||||
} else {
|
} else {
|
||||||
LOGGER.debug("Lock not frozen - ignore Call");
|
LOGGER.debug("Lock not frozen - ignore Call");
|
||||||
}
|
}
|
||||||
@@ -358,7 +360,6 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
// ── Hygiene opening ───────────────────────────────────────────────────────
|
// ── Hygiene opening ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void startHygieneOpening() {
|
public void startHygieneOpening() {
|
||||||
lockControl.unlock();
|
|
||||||
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package de.oaa.xxx.games.chastity.timelock;
|
package de.oaa.xxx.games.chastity.timelock;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityPilloryRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityPilloryRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||||
@@ -32,6 +36,7 @@ public class TimeLockServiceFactory {
|
|||||||
private final SystemMessageService systemMessageService;
|
private final SystemMessageService systemMessageService;
|
||||||
private CommunityVerificationVoteRepository communityVerificationVoteRepository;
|
private CommunityVerificationVoteRepository communityVerificationVoteRepository;
|
||||||
private final LockControlFactory lockControlFactory;
|
private final LockControlFactory lockControlFactory;
|
||||||
|
private final CardlockRepository cardlockRepository;
|
||||||
|
|
||||||
public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository,
|
public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository,
|
||||||
CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository,
|
CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository,
|
||||||
@@ -41,7 +46,7 @@ public class TimeLockServiceFactory {
|
|||||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||||
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository,
|
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository,
|
||||||
UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService,
|
UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService,
|
||||||
LockControlFactory lockControlFactory) {
|
LockControlFactory lockControlFactory, CardlockRepository cardlockRepository) {
|
||||||
this.communityVerificationVoteRepository = verificationVoteRepository;
|
this.communityVerificationVoteRepository = verificationVoteRepository;
|
||||||
this.timeLockRepository = timeLockRepository;
|
this.timeLockRepository = timeLockRepository;
|
||||||
this.communityVerificationRepository = verificationRepository;
|
this.communityVerificationRepository = verificationRepository;
|
||||||
@@ -55,6 +60,11 @@ public class TimeLockServiceFactory {
|
|||||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||||
this.keyholderVerificationRepository = keyholderVerificationRepository;
|
this.keyholderVerificationRepository = keyholderVerificationRepository;
|
||||||
this.lockControlFactory = lockControlFactory;
|
this.lockControlFactory = lockControlFactory;
|
||||||
|
this.cardlockRepository = cardlockRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasActiveLock(UUID lockeeId) {
|
||||||
|
return BaseLockService.hasActiveLock(lockeeId, cardlockRepository, timeLockRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -365,7 +365,8 @@ public class SocialController {
|
|||||||
user.getSichtbarkeitFeed(),
|
user.getSichtbarkeitFeed(),
|
||||||
user.getSichtbarkeitPinnwand(),
|
user.getSichtbarkeitPinnwand(),
|
||||||
user.getSichtbarkeitXp(),
|
user.getSichtbarkeitXp(),
|
||||||
user.getSichtbarkeitLockhistorie());
|
user.getSichtbarkeitLockhistorie(),
|
||||||
|
user.isProfilBeiVeroeffentlichungenSichtbar());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MessageDto toMessageDto(MessageEntity m) {
|
private MessageDto toMessageDto(MessageEntity m) {
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ public record UserProfile(
|
|||||||
Sichtbarkeit sichtbarkeitFeed,
|
Sichtbarkeit sichtbarkeitFeed,
|
||||||
Sichtbarkeit sichtbarkeitPinnwand,
|
Sichtbarkeit sichtbarkeitPinnwand,
|
||||||
Sichtbarkeit sichtbarkeitXp,
|
Sichtbarkeit sichtbarkeitXp,
|
||||||
Sichtbarkeit sichtbarkeitLockhistorie
|
Sichtbarkeit sichtbarkeitLockhistorie,
|
||||||
|
boolean profilBeiVeroeffentlichungenSichtbar
|
||||||
) {
|
) {
|
||||||
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */
|
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */
|
||||||
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
|
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
|
||||||
this(userId, name, profilePicture, profilePictureHq, friendStatus,
|
this(userId, name, profilePicture, profilePictureHq, friendStatus,
|
||||||
null, null, null, null, null, null, null, 0, 0, 0,
|
null, null, null, null, null, null, null, 0, 0, 0,
|
||||||
null, null, null, null, null, null, null);
|
null, null, null, null, null, null, null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import java.util.UUID;
|
|||||||
public class SubscriptionLimitService {
|
public class SubscriptionLimitService {
|
||||||
|
|
||||||
// ── Limits for STANDARD (no active subscription) ──
|
// ── Limits for STANDARD (no active subscription) ──
|
||||||
public static final int STANDARD_MAX_LOCK_TEMPLATES = 6;
|
public static final int STANDARD_MAX_LOCK_TEMPLATES = 6;
|
||||||
public static final int STANDARD_MAX_TASK_GROUPS = 6;
|
public static final int STANDARD_MAX_TASK_GROUPS = 6;
|
||||||
public static final int STANDARD_MAX_TASKS_PER_GROUP = 50;
|
public static final int STANDARD_MAX_TASKS_PER_GROUP = 50;
|
||||||
public static final int STANDARD_MAX_TOYS = 10;
|
public static final int STANDARD_MAX_TOYS = 10;
|
||||||
|
public static final int STANDARD_MAX_KEYHOLDER_OFFERS = 5;
|
||||||
|
|
||||||
private final UserSubscriptionRepository subscriptionRepository;
|
private final UserSubscriptionRepository subscriptionRepository;
|
||||||
|
|
||||||
@@ -61,4 +62,10 @@ public class SubscriptionLimitService {
|
|||||||
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||||
return STANDARD_MAX_TOYS;
|
return STANDARD_MAX_TOYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max keyholder offers the user may create. */
|
||||||
|
public int maxKeyholderOffers(UUID userId) {
|
||||||
|
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||||
|
return STANDARD_MAX_KEYHOLDER_OFFERS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
|
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
|
||||||
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
|
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
|
||||||
import de.oaa.xxx.games.chastity.common.BaseLockRepository;
|
import de.oaa.xxx.games.chastity.common.BaseLockRepository;
|
||||||
|
import de.oaa.xxx.games.chastity.common.BaseLockTemplateRepository;
|
||||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||||
import de.oaa.xxx.games.chastity.ttlock.TTAuthService;
|
import de.oaa.xxx.games.chastity.ttlock.TTAuthService;
|
||||||
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
|
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
|
||||||
@@ -57,6 +58,7 @@ public class UserController {
|
|||||||
private final TTAuthService ttAuthService;
|
private final TTAuthService ttAuthService;
|
||||||
private final TTLockService ttLockService;
|
private final TTLockService ttLockService;
|
||||||
private final BaseLockRepository baseLockRepository;
|
private final BaseLockRepository baseLockRepository;
|
||||||
|
private final BaseLockTemplateRepository baseLockTemplateRepository;
|
||||||
|
|
||||||
public UserController(UserRepository userRepository,
|
public UserController(UserRepository userRepository,
|
||||||
RegistrationRepository registrationRepository,
|
RegistrationRepository registrationRepository,
|
||||||
@@ -67,7 +69,8 @@ public class UserController {
|
|||||||
TTLockConfigRepository ttLockConfigRepository,
|
TTLockConfigRepository ttLockConfigRepository,
|
||||||
TTAuthService ttAuthService,
|
TTAuthService ttAuthService,
|
||||||
TTLockService ttLockService,
|
TTLockService ttLockService,
|
||||||
BaseLockRepository baseLockRepository) {
|
BaseLockRepository baseLockRepository,
|
||||||
|
BaseLockTemplateRepository baseLockTemplateRepository) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.registrationRepository = registrationRepository;
|
this.registrationRepository = registrationRepository;
|
||||||
this.notificationPreferenceRepository = notificationPreferenceRepository;
|
this.notificationPreferenceRepository = notificationPreferenceRepository;
|
||||||
@@ -78,6 +81,7 @@ public class UserController {
|
|||||||
this.ttAuthService = ttAuthService;
|
this.ttAuthService = ttAuthService;
|
||||||
this.ttLockService = ttLockService;
|
this.ttLockService = ttLockService;
|
||||||
this.baseLockRepository = baseLockRepository;
|
this.baseLockRepository = baseLockRepository;
|
||||||
|
this.baseLockTemplateRepository = baseLockTemplateRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||||
@@ -94,7 +98,8 @@ public class UserController {
|
|||||||
Sichtbarkeit sichtbarkeitFeed,
|
Sichtbarkeit sichtbarkeitFeed,
|
||||||
Sichtbarkeit sichtbarkeitPinnwand,
|
Sichtbarkeit sichtbarkeitPinnwand,
|
||||||
Sichtbarkeit sichtbarkeitXp,
|
Sichtbarkeit sichtbarkeitXp,
|
||||||
Sichtbarkeit sichtbarkeitLockhistorie) {}
|
Sichtbarkeit sichtbarkeitLockhistorie,
|
||||||
|
Boolean profilBeiVeroeffentlichungenSichtbar) {}
|
||||||
|
|
||||||
@PutMapping("/me/picture")
|
@PutMapping("/me/picture")
|
||||||
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
||||||
@@ -138,6 +143,16 @@ public class UserController {
|
|||||||
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
|
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
|
||||||
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
|
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
|
||||||
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
|
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
|
||||||
|
if (request.profilBeiVeroeffentlichungenSichtbar() != null) {
|
||||||
|
boolean showAuthor = request.profilBeiVeroeffentlichungenSichtbar();
|
||||||
|
user.setProfilBeiVeroeffentlichungenSichtbar(showAuthor);
|
||||||
|
// Alle veröffentlichten Templates synchronisieren
|
||||||
|
var templates = baseLockTemplateRepository.findByOwnerAndPublishedTrue(user.getUserId());
|
||||||
|
for (var t : templates) {
|
||||||
|
t.setShowAuthor(showAuthor);
|
||||||
|
}
|
||||||
|
baseLockTemplateRepository.saveAll(templates);
|
||||||
|
}
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
|
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ public class UserEntity {
|
|||||||
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
|
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
|
||||||
private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE;
|
private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||||
|
private boolean profilBeiVeroeffentlichungenSichtbar = false;
|
||||||
|
|
||||||
public Integer getAlter() {
|
public Integer getAlter() {
|
||||||
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
|
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -678,6 +678,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Temp-Öffnung-Modal (CARD / TASK / etc.) -->
|
||||||
|
<div class="hygiene-modal-backdrop" id="tempOpeningModal">
|
||||||
|
<div class="hygiene-modal-box">
|
||||||
|
<div class="hygiene-modal-icon">🔓</div>
|
||||||
|
<h3>Lock geöffnet</h3>
|
||||||
|
|
||||||
|
<!-- Phase 1: Code anzeigen -->
|
||||||
|
<div id="tempPhase1" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
|
||||||
|
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein aktueller Entsperrcode:</p>
|
||||||
|
<div class="hygiene-unlock-code" id="tempOpeningCode">–</div>
|
||||||
|
<button class="btn-draw-ok" style="margin-top:0.25rem;" onclick="endTempOpeningFlow()">🔒 Öffnung beenden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phase 2: Neuer Code (nur UNLOCK_CODE) -->
|
||||||
|
<div id="tempPhase2" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.75rem;">
|
||||||
|
<p id="tempPhase2Hint" style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein neuer Code zum Abschließen:</p>
|
||||||
|
<div class="hygiene-unlock-code" id="tempNewCode">–</div>
|
||||||
|
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
||||||
|
<button class="btn-draw-ok" id="tempPhase2Btn" onclick="startTempScramble()">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Karte-Ziehen-Modal -->
|
<!-- Karte-Ziehen-Modal -->
|
||||||
<div class="draw-modal-backdrop" id="drawModal">
|
<div class="draw-modal-backdrop" id="drawModal">
|
||||||
<div class="draw-modal-box">
|
<div class="draw-modal-box">
|
||||||
@@ -811,6 +834,7 @@
|
|||||||
renderNextCardPanel(lock);
|
renderNextCardPanel(lock);
|
||||||
renderHygienePanel(lock);
|
renderHygienePanel(lock);
|
||||||
renderVerificationPanel(lock);
|
renderVerificationPanel(lock);
|
||||||
|
renderTempOpeningPanel(lock);
|
||||||
renderCardsPanel(lock);
|
renderCardsPanel(lock);
|
||||||
|
|
||||||
if (lock.keyholderRequestedUnlock) {
|
if (lock.keyholderRequestedUnlock) {
|
||||||
@@ -1508,6 +1532,111 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Temp-Öffnung (CARD / TASK / etc.) ──
|
||||||
|
|
||||||
|
function renderTempOpeningPanel(lock) {
|
||||||
|
if (lock.tempOpeningActive) {
|
||||||
|
document.getElementById('tempOpeningCode').textContent = lock.tempOpeningUnlockCode || '–';
|
||||||
|
document.getElementById('tempPhase1').style.display = '';
|
||||||
|
document.getElementById('tempPhase2').style.display = 'none';
|
||||||
|
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
|
||||||
|
document.getElementById('tempOpeningModal').classList.add('open');
|
||||||
|
} else {
|
||||||
|
document.getElementById('tempOpeningModal').classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function endTempOpeningFlow() {
|
||||||
|
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
|
||||||
|
const isTrust = _currentLock && _currentLock.controllType === 'TRUST';
|
||||||
|
|
||||||
|
if (isTtlock) {
|
||||||
|
document.getElementById('tempOpeningModal').classList.remove('open');
|
||||||
|
document.getElementById('ttlLoadingModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
|
||||||
|
|
||||||
|
if (isTtlock) {
|
||||||
|
document.getElementById('ttlLoadingModal').classList.remove('open');
|
||||||
|
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
|
||||||
|
loadLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (isTrust || !data.newUnlockCode) {
|
||||||
|
document.getElementById('tempOpeningModal').classList.remove('open');
|
||||||
|
loadLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNLOCK_CODE: neuen Code anzeigen
|
||||||
|
document.getElementById('tempPhase1').style.display = 'none';
|
||||||
|
document.getElementById('tempPhase2').style.display = 'flex';
|
||||||
|
document.getElementById('tempNewCode').textContent = data.newUnlockCode;
|
||||||
|
document.getElementById('tempPhase2Hint').style.display = '';
|
||||||
|
document.getElementById('tempPhase2Btn').textContent = 'OK';
|
||||||
|
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
|
||||||
|
document.getElementById('tempScrambleCountdown').style.display = 'none';
|
||||||
|
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTempOpeningModal() {
|
||||||
|
document.getElementById('tempOpeningModal').classList.remove('open');
|
||||||
|
if (tempScrambleTimer) { clearInterval(tempScrambleTimer); tempScrambleTimer = null; }
|
||||||
|
if (tempScrambleCd) { clearInterval(tempScrambleCd); tempScrambleCd = null; }
|
||||||
|
loadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempScrambleTimer = null;
|
||||||
|
let tempScrambleCd = null;
|
||||||
|
|
||||||
|
function startTempScramble() {
|
||||||
|
const codeEl = document.getElementById('tempNewCode');
|
||||||
|
const hintEl = document.getElementById('tempPhase2Hint');
|
||||||
|
const cdEl = document.getElementById('tempScrambleCountdown');
|
||||||
|
const btnEl = document.getElementById('tempPhase2Btn');
|
||||||
|
const realCode = codeEl.textContent;
|
||||||
|
const len = realCode.length;
|
||||||
|
const DURATION = 3 * 60;
|
||||||
|
let remaining = DURATION;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
function randomCode() {
|
||||||
|
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||||||
|
}
|
||||||
|
function finish() {
|
||||||
|
stopped = true;
|
||||||
|
clearInterval(tempScrambleTimer); tempScrambleTimer = null;
|
||||||
|
clearInterval(tempScrambleCd); tempScrambleCd = null;
|
||||||
|
closeTempOpeningModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
hintEl.style.display = 'none';
|
||||||
|
cdEl.style.display = '';
|
||||||
|
document.querySelector('#tempOpeningModal h3').textContent = 'Nun vergessen wir den Code…';
|
||||||
|
btnEl.textContent = 'Abbrechen';
|
||||||
|
btnEl.onclick = finish;
|
||||||
|
|
||||||
|
function updateCd() {
|
||||||
|
const m = Math.floor(remaining / 60);
|
||||||
|
const s = remaining % 60;
|
||||||
|
cdEl.textContent = `${m}:${String(s).padStart(2,'0')}`;
|
||||||
|
}
|
||||||
|
updateCd();
|
||||||
|
|
||||||
|
tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
|
||||||
|
tempScrambleCd = setInterval(() => {
|
||||||
|
if (stopped) return;
|
||||||
|
remaining--;
|
||||||
|
updateCd();
|
||||||
|
if (remaining <= 0) finish();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Lock beenden ──
|
// ── Lock beenden ──
|
||||||
function lockBeendenFragen() {
|
function lockBeendenFragen() {
|
||||||
document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '–') : '–';
|
document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '–') : '–';
|
||||||
|
|||||||
@@ -183,21 +183,14 @@
|
|||||||
}
|
}
|
||||||
#wheelCanvas { border-radius: 50%; display: block; }
|
#wheelCanvas { border-radius: 50%; display: block; }
|
||||||
|
|
||||||
/* ── Spin-Result-Modal ── */
|
/* ── Glücksrad-Ergebnis (unterhalb des Rads) ── */
|
||||||
.spin-modal-backdrop {
|
.wheel-result {
|
||||||
display: none; position: fixed; inset: 0;
|
margin-top: 1.25rem; width: 290px;
|
||||||
background: rgba(0,0,0,0.65); z-index: 500;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
}
|
|
||||||
.spin-modal-backdrop.open { display: flex; }
|
|
||||||
.spin-modal-box {
|
|
||||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
|
||||||
border-radius: 14px; padding: 2rem 1.75rem 1.5rem; max-width: 340px; width: 90%;
|
|
||||||
display: flex; flex-direction: column; align-items: center;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
gap: 1rem; text-align: center;
|
gap: 0.6rem; text-align: center;
|
||||||
}
|
}
|
||||||
.spin-result-icon { font-size: 3rem; }
|
.spin-result-icon { font-size: 3rem; }
|
||||||
.spin-result-title { font-size: 1.1rem; font-weight: 700; margin: 0; }
|
.spin-result-title { font-size: 1.1rem; font-weight: 700; margin: 0; color: var(--color-text); }
|
||||||
.spin-result-desc { font-size: 0.9rem; color: var(--color-muted); line-height: 1.5; margin: 0; }
|
.spin-result-desc { font-size: 0.9rem; color: var(--color-muted); line-height: 1.5; margin: 0; }
|
||||||
|
|
||||||
/* ── Hygiene-Modal ── */
|
/* ── Hygiene-Modal ── */
|
||||||
@@ -268,6 +261,17 @@
|
|||||||
border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s;
|
border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s;
|
||||||
}
|
}
|
||||||
.btn-lock-unlock:hover { background: rgba(46,204,113,0.28); }
|
.btn-lock-unlock:hover { background: rgba(46,204,113,0.28); }
|
||||||
|
.btn-confirm-unlock {
|
||||||
|
background: rgba(52,152,219,0.15); border: 1px solid rgba(52,152,219,0.45);
|
||||||
|
color: #3498db; font-size: 0.88rem; font-weight: 600; padding: 0.55rem 1.25rem;
|
||||||
|
border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.btn-confirm-unlock:hover { background: rgba(52,152,219,0.28); }
|
||||||
|
.unlock-confirmed-badge {
|
||||||
|
font-size: 0.82rem; color: #2ecc71;
|
||||||
|
border: 1px solid rgba(46,204,113,0.35); border-radius: 7px;
|
||||||
|
padding: 0.35rem 0.75rem; display: inline-block;
|
||||||
|
}
|
||||||
.btn-lock-beenden {
|
.btn-lock-beenden {
|
||||||
background: transparent; border: 1px solid rgba(200,50,50,0.45);
|
background: transparent; border: 1px solid rgba(200,50,50,0.45);
|
||||||
color: rgba(200,50,50,0.7); font-size: 0.82rem; padding: 0.5rem 1rem;
|
color: rgba(200,50,50,0.7); font-size: 0.82rem; padding: 0.5rem 1rem;
|
||||||
@@ -368,16 +372,12 @@
|
|||||||
<div class="wheel-canvas-wrap">
|
<div class="wheel-canvas-wrap">
|
||||||
<div class="wheel-pointer-top">▼</div>
|
<div class="wheel-pointer-top">▼</div>
|
||||||
<canvas id="wheelCanvas" width="290" height="290"></canvas>
|
<canvas id="wheelCanvas" width="290" height="290"></canvas>
|
||||||
</div>
|
<div class="wheel-result" id="wheelResult" style="display:none;">
|
||||||
</div>
|
<div class="spin-result-icon" id="spinResultIcon"></div>
|
||||||
|
<h3 class="spin-result-title" id="spinResultTitle"></h3>
|
||||||
<!-- Spin-Result-Modal -->
|
<p class="spin-result-desc" id="spinResultDesc"></p>
|
||||||
<div class="spin-modal-backdrop" id="spinModal">
|
<button onclick="closeSpinModal()" style="width:100%;margin-top:0.25rem;">OK</button>
|
||||||
<div class="spin-modal-box">
|
</div>
|
||||||
<div class="spin-result-icon" id="spinResultIcon"></div>
|
|
||||||
<h3 class="spin-result-title" id="spinResultTitle"></h3>
|
|
||||||
<p class="spin-result-desc" id="spinResultDesc"></p>
|
|
||||||
<button onclick="closeSpinModal()" style="width:100%;margin-top:0.25rem;">OK</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -403,6 +403,12 @@
|
|||||||
<div id="hygieneScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
<div id="hygieneScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
|
||||||
<button id="hygienePhase2Btn" onclick="startHygieneScramble()" style="background:var(--color-primary);color:#fff;border:none;padding:0.55rem 1.5rem;border-radius:8px;cursor:pointer;font-weight:600;">OK</button>
|
<button id="hygienePhase2Btn" onclick="startHygieneScramble()" style="background:var(--color-primary);color:#fff;border:none;padding:0.55rem 1.5rem;border-radius:8px;cursor:pointer;font-weight:600;">OK</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Phase 3: TTLock-Kommunikation -->
|
||||||
|
<div id="hygienePhase3" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.5rem;padding:0.5rem 0;">
|
||||||
|
<div style="font-size:1.5rem;">🔗</div>
|
||||||
|
<div style="font-size:0.9rem;color:var(--color-muted);">Kommuniziere mit TTLock-Server…</div>
|
||||||
|
<div style="font-size:0.8rem;color:var(--color-muted);font-family:monospace;">Bitte warten</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -430,6 +436,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unlock-Result-Modal -->
|
||||||
|
<div class="warn-modal-backdrop" id="unlockResultModal">
|
||||||
|
<div class="warn-modal-box" style="align-items:center;text-align:center;">
|
||||||
|
<div style="font-size:2.5rem;line-height:1;">🔓</div>
|
||||||
|
<h3 style="margin:0;">Lock entsperrt</h3>
|
||||||
|
<div id="unlockResultBody" style="width:100%;"></div>
|
||||||
|
<div class="warn-modal-actions" style="justify-content:center;flex-wrap:wrap;margin-top:0.5rem;">
|
||||||
|
<button class="btn-cancel" onclick="document.getElementById('unlockResultModal').classList.remove('open')">Schließen</button>
|
||||||
|
<button style="background:#27ae60;border:none;color:#fff;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;" onclick="lockLoeschen()">Lock beenden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Warn-Modal (Lock beenden) -->
|
<!-- Warn-Modal (Lock beenden) -->
|
||||||
<div class="warn-modal-backdrop" id="warnModal">
|
<div class="warn-modal-backdrop" id="warnModal">
|
||||||
<div class="warn-modal-box">
|
<div class="warn-modal-box">
|
||||||
@@ -637,6 +656,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function startWheelSpin() {
|
function startWheelSpin() {
|
||||||
|
document.getElementById('wheelResult').style.display = 'none';
|
||||||
document.getElementById('wheelAnimModal').classList.add('open');
|
document.getElementById('wheelAnimModal').classList.add('open');
|
||||||
wheelAngle = Math.random() * 2 * Math.PI;
|
wheelAngle = Math.random() * 2 * Math.PI;
|
||||||
wheelResult = null;
|
wheelResult = null;
|
||||||
@@ -673,9 +693,7 @@
|
|||||||
wheelAnimState = 'done';
|
wheelAnimState = 'done';
|
||||||
drawWheelFrame(canvas, wheelAngle);
|
drawWheelFrame(canvas, wheelAngle);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('wheelAnimModal').classList.remove('open');
|
|
||||||
showSpinResult(wheelResult);
|
showSpinResult(wheelResult);
|
||||||
loadLock();
|
|
||||||
}, 750);
|
}, 750);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -777,7 +795,7 @@
|
|||||||
panel.style.display = '';
|
panel.style.display = '';
|
||||||
|
|
||||||
if (lock.spinDue) {
|
if (lock.spinDue) {
|
||||||
cdEl.textContent = 'Jetzt fällig';
|
cdEl.textContent = 'Bereit';
|
||||||
cdEl.style.color = '#2ecc71';
|
cdEl.style.color = '#2ecc71';
|
||||||
btn.disabled = lock.isFrozen || lock.hygieneOpeningActive || false;
|
btn.disabled = lock.isFrozen || lock.hygieneOpeningActive || false;
|
||||||
return;
|
return;
|
||||||
@@ -792,7 +810,7 @@
|
|||||||
function tick() {
|
function tick() {
|
||||||
const diff = target - Date.now();
|
const diff = target - Date.now();
|
||||||
if (diff <= 0) {
|
if (diff <= 0) {
|
||||||
cdEl.textContent = 'Jetzt fällig';
|
cdEl.textContent = 'Bereit';
|
||||||
cdEl.style.color = '#2ecc71';
|
cdEl.style.color = '#2ecc71';
|
||||||
btn.disabled = lock.isFrozen || false;
|
btn.disabled = lock.isFrozen || false;
|
||||||
clearInterval(spinTickInterval); spinTickInterval = null;
|
clearInterval(spinTickInterval); spinTickInterval = null;
|
||||||
@@ -857,11 +875,13 @@
|
|||||||
document.getElementById('spinResultIcon').textContent = info.icon;
|
document.getElementById('spinResultIcon').textContent = info.icon;
|
||||||
document.getElementById('spinResultTitle').textContent = info.title;
|
document.getElementById('spinResultTitle').textContent = info.title;
|
||||||
document.getElementById('spinResultDesc').textContent = info.descFn(result);
|
document.getElementById('spinResultDesc').textContent = info.descFn(result);
|
||||||
document.getElementById('spinModal').classList.add('open');
|
document.getElementById('wheelResult').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSpinModal() {
|
function closeSpinModal() {
|
||||||
document.getElementById('spinModal').classList.remove('open');
|
document.getElementById('wheelAnimModal').classList.remove('open');
|
||||||
|
document.getElementById('wheelResult').style.display = 'none';
|
||||||
|
loadLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Aufgaben-Panel ─────────────────────────────────────────────────────────
|
// ── Aufgaben-Panel ─────────────────────────────────────────────────────────
|
||||||
@@ -992,10 +1012,25 @@
|
|||||||
async function endHygieneOpening() {
|
async function endHygieneOpening() {
|
||||||
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
||||||
|
|
||||||
|
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
|
||||||
|
document.getElementById('hygienePhase1').style.display = 'none';
|
||||||
|
document.getElementById('hygienePhase3').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch('/keyholder/timelock/' + lockId + '/hygiene/end', { method: 'POST' });
|
const res = await fetch('/keyholder/timelock/' + lockId + '/hygiene/end', { method: 'POST' });
|
||||||
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
if (!res.ok) {
|
||||||
|
document.getElementById('hygienePhase3').style.display = 'none';
|
||||||
|
document.getElementById('hygienePhase1').style.display = 'flex';
|
||||||
|
alert('Fehler beim Beenden der Hygiene-Öffnung.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
|
||||||
|
closeHygieneModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('hygienePhase1').style.display = 'none';
|
document.getElementById('hygienePhase1').style.display = 'none';
|
||||||
const phase2 = document.getElementById('hygienePhase2');
|
const phase2 = document.getElementById('hygienePhase2');
|
||||||
phase2.style.display = 'flex';
|
phase2.style.display = 'flex';
|
||||||
@@ -1169,9 +1204,32 @@
|
|||||||
if (!area) return;
|
if (!area) return;
|
||||||
|
|
||||||
if (lock.timeUp) {
|
if (lock.timeUp) {
|
||||||
// Unlock-Button anzeigen
|
if (!lock.actualUnlockTime) {
|
||||||
|
area.innerHTML = `<button class="btn-lock-unlock" onclick="confirmUnlock()">🔓 Entsperren</button>`;
|
||||||
|
} else {
|
||||||
|
area.innerHTML = `<button class="btn-lock-unlock" onclick="openUnlockResultModal()">🔓 Lock abschließen</button>`;
|
||||||
|
}
|
||||||
|
} else if (lock.keyholderRequestedUnlock) {
|
||||||
|
const code = lock.unlockCode || '–';
|
||||||
|
const isEmergency = lock.emergencyAutoUnlocked;
|
||||||
|
const title = isEmergency
|
||||||
|
? '🆘 Lock automatisch freigegeben'
|
||||||
|
: '🔓 Dein Keyholder hat das Lock freigegeben!';
|
||||||
|
const note = isEmergency
|
||||||
|
? `<div style="font-size:0.8rem;color:var(--color-muted);margin-top:0.4rem;">
|
||||||
|
Da dein Keyholder nicht innerhalb einer Stunde reagiert hat, wurde das Lock automatisch geöffnet. Es werden keine XP vergeben und kein Eintrag in die Historie gespeichert.
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
area.innerHTML = `
|
area.innerHTML = `
|
||||||
<button class="btn-lock-unlock" onclick="lockOeffnen()">🔓 Lock öffnen</button>`;
|
<div style="background:rgba(46,204,113,0.06);border:1px solid rgba(46,204,113,0.3);border-radius:10px;padding:1rem 1.1rem;">
|
||||||
|
<div style="font-weight:700;font-size:0.95rem;color:#2ecc71;margin-bottom:0.6rem;">${title}</div>
|
||||||
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.07em;margin-bottom:0.3rem;">Dein Entsperrcode</div>
|
||||||
|
<div style="font-size:2rem;font-weight:700;font-family:monospace;letter-spacing:0.2em;
|
||||||
|
background:rgba(233,69,96,0.08);border:1px solid rgba(233,69,96,0.25);
|
||||||
|
border-radius:8px;padding:0.5rem 1rem;color:var(--color-primary);text-align:center;">${code}</div>
|
||||||
|
${note}
|
||||||
|
<button class="btn-lock-beenden" style="margin-top:0.75rem;" onclick="lockLoeschen()">🔓 Lock beenden</button>
|
||||||
|
</div>`;
|
||||||
} else if (lock.testLock) {
|
} else if (lock.testLock) {
|
||||||
area.innerHTML = `<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>`;
|
area.innerHTML = `<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>`;
|
||||||
} else if (lock.emergencyUnlockRequested) {
|
} else if (lock.emergencyUnlockRequested) {
|
||||||
@@ -1188,11 +1246,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockOeffnen() {
|
async function confirmUnlock() {
|
||||||
const code = _currentLock ? (_currentLock.unlockCode || '') : '';
|
const btn = document.querySelector('#lockActionArea .btn-lock-unlock');
|
||||||
document.getElementById('warnModalUnlockCode').textContent = code || '(wird nach Bestätigung angezeigt)';
|
if (btn) btn.disabled = true;
|
||||||
document.getElementById('warnModal').classList.add('open');
|
try {
|
||||||
document.querySelector('#warnModal h3').textContent = '🔓 Lock öffnen?';
|
const res = await fetch('/keyholder/timelock/' + lockId + '/unlock-time', { method: 'PATCH' });
|
||||||
|
if (res.ok || res.status === 204) {
|
||||||
|
const r2 = await fetch('/keyholder/timelock/' + lockId);
|
||||||
|
if (r2.ok) _currentLock = await r2.json();
|
||||||
|
renderLockActionArea(_currentLock);
|
||||||
|
openUnlockResultModal();
|
||||||
|
} else {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch(_) {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUnlockResultModal() {
|
||||||
|
const lock = _currentLock;
|
||||||
|
const body = document.getElementById('unlockResultBody');
|
||||||
|
if (lock && lock.controllType === 'TRUST') {
|
||||||
|
body.innerHTML = `<p style="margin:0.5rem 0 0;color:#2ecc71;font-weight:600;font-size:1rem;">✓ Du kannst dich jetzt befreien.</p>`;
|
||||||
|
} else {
|
||||||
|
const code = (lock && lock.unlockCode) || '–';
|
||||||
|
body.innerHTML = `
|
||||||
|
<div style="margin-top:0.75rem;">
|
||||||
|
<div style="font-size:0.75rem;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.07em;margin-bottom:0.4rem;">Entsperrcode</div>
|
||||||
|
<div style="font-size:2.2rem;font-weight:700;font-family:monospace;letter-spacing:0.22em;
|
||||||
|
background:rgba(233,69,96,0.08);border:1px solid rgba(233,69,96,0.25);
|
||||||
|
border-radius:8px;padding:0.6rem 1.4rem;color:var(--color-primary);">${code}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
document.getElementById('unlockResultModal').classList.add('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockBeendenFragen() {
|
function lockBeendenFragen() {
|
||||||
@@ -1207,6 +1294,17 @@
|
|||||||
|
|
||||||
async function lockLoeschen() {
|
async function lockLoeschen() {
|
||||||
closeWarnModal();
|
closeWarnModal();
|
||||||
|
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
|
||||||
|
document.getElementById('unlockResultBody').innerHTML = `
|
||||||
|
<div style="margin-top:0.75rem;display:flex;flex-direction:column;align-items:center;gap:0.5rem;">
|
||||||
|
<div style="font-size:1.5rem;">🔗</div>
|
||||||
|
<div style="font-size:0.9rem;color:var(--color-muted);">Kommuniziere mit TTLock-Server…</div>
|
||||||
|
<div style="font-size:0.8rem;color:var(--color-muted);font-family:monospace;">Bitte warten</div>
|
||||||
|
</div>`;
|
||||||
|
// Buttons im Modal ausblenden während kommuniziert wird
|
||||||
|
document.querySelector('#unlockResultModal .warn-modal-actions').style.display = 'none';
|
||||||
|
document.getElementById('unlockResultModal').classList.add('open');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' });
|
await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' });
|
||||||
} catch(_) { /* ignorieren */ }
|
} catch(_) { /* ignorieren */ }
|
||||||
@@ -1273,6 +1371,7 @@
|
|||||||
closeWarnModal();
|
closeWarnModal();
|
||||||
closeEmergencyModal();
|
closeEmergencyModal();
|
||||||
closeSpinModal();
|
closeSpinModal();
|
||||||
|
document.getElementById('unlockResultModal').classList.remove('open');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -304,6 +304,34 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Keyholder-Angebote Tab ── */
|
||||||
|
.kh-offer-card {
|
||||||
|
background:var(--color-card); border:1px solid var(--color-secondary);
|
||||||
|
border-radius:10px; padding:0.75rem 1rem; margin-bottom:0.6rem;
|
||||||
|
display:flex; align-items:center; gap:0.85rem;
|
||||||
|
}
|
||||||
|
.kh-offer-type-icon {
|
||||||
|
position:relative; width:2.2rem; height:2.2rem; flex-shrink:0;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.kh-offer-type-icon .icon-base { font-size:1.8rem; line-height:1; }
|
||||||
|
.kh-offer-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; }
|
||||||
|
.kh-offer-type-icon .icon-lock {
|
||||||
|
position:absolute; bottom:-2px; right:-4px;
|
||||||
|
font-size:1.5rem; line-height:1;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||||
|
}
|
||||||
|
.kh-offer-body { flex:1; min-width:0; }
|
||||||
|
.kh-offer-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem;
|
||||||
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||||
|
.kh-offer-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||||
|
.kh-offer-badge {
|
||||||
|
display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem;
|
||||||
|
border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.kh-offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; }
|
||||||
|
.kh-offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; }
|
||||||
|
|
||||||
/* ── Comments (section container) ── */
|
/* ── Comments (section container) ── */
|
||||||
.comments-section {
|
.comments-section {
|
||||||
margin-top: 0.65rem;
|
margin-top: 0.65rem;
|
||||||
@@ -406,11 +434,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs: Feed | Pinnwand | Spielhistorie -->
|
<!-- Tabs: Feed | Pinnwand | Spielhistorie | Keyholder-Angebote -->
|
||||||
<div class="profil-tabs" style="margin-top:1.25rem;">
|
<div class="profil-tabs" style="margin-top:1.25rem;">
|
||||||
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
|
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
|
||||||
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
|
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
|
||||||
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
|
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
|
||||||
|
<button class="profil-tab-btn" id="tabBtnKhOffers" onclick="switchProfilTab('khoffers', this)">Keyholder-Angebote</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Feed Tab (vorausgewählt) -->
|
<!-- Feed Tab (vorausgewählt) -->
|
||||||
@@ -435,6 +464,12 @@
|
|||||||
<p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
|
<p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Keyholder-Angebote Tab -->
|
||||||
|
<div class="profil-tab-panel" id="tab-khoffers">
|
||||||
|
<div id="khOffersList" style="margin-top:0.75rem;"></div>
|
||||||
|
<p id="khOffersEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine Keyholder-Angebote vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -682,6 +717,7 @@
|
|||||||
|
|
||||||
// ── Tab switching ──
|
// ── Tab switching ──
|
||||||
let _gameHistoryLoaded = false;
|
let _gameHistoryLoaded = false;
|
||||||
|
let _khOffersLoaded = false;
|
||||||
function switchProfilTab(name, btn) {
|
function switchProfilTab(name, btn) {
|
||||||
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
@@ -691,6 +727,10 @@
|
|||||||
_gameHistoryLoaded = true;
|
_gameHistoryLoaded = true;
|
||||||
loadGameHistory();
|
loadGameHistory();
|
||||||
}
|
}
|
||||||
|
if (name === 'khoffers' && !_khOffersLoaded) {
|
||||||
|
_khOffersLoaded = true;
|
||||||
|
loadKhOffers();
|
||||||
|
}
|
||||||
// URL-QueryParam aktualisieren, damit der Tab nach F5 erhalten bleibt
|
// URL-QueryParam aktualisieren, damit der Tab nach F5 erhalten bleibt
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
if (name === 'posts') {
|
if (name === 'posts') {
|
||||||
@@ -946,6 +986,49 @@
|
|||||||
} catch(e) { list.innerHTML = ''; }
|
} catch(e) { list.innerHTML = ''; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Keyholder-Angebote ──
|
||||||
|
const KH_GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
|
||||||
|
|
||||||
|
async function loadKhOffers() {
|
||||||
|
const list = document.getElementById('khOffersList');
|
||||||
|
const empty = document.getElementById('khOffersEmpty');
|
||||||
|
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/keyholder-offers/user/' + targetUserId);
|
||||||
|
if (!res.ok) { list.innerHTML = ''; return; }
|
||||||
|
const offers = await res.json();
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (offers.length === 0) { empty.style.display = ''; return; }
|
||||||
|
offers.forEach(o => list.appendChild(buildKhOfferCard(o)));
|
||||||
|
} catch(e) { list.innerHTML = ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKhOfferCard(o) {
|
||||||
|
const esc = s => { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; };
|
||||||
|
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
|
||||||
|
? o.targetGenders.map(g => `<span class="kh-offer-badge">${esc(KH_GENDER_LABELS[g] || g)}</span>`).join('')
|
||||||
|
: '<span class="kh-offer-badge">Alle</span>';
|
||||||
|
const modeBadge = o.directStart
|
||||||
|
? '<span class="kh-offer-badge direct">Direktstart</span>'
|
||||||
|
: '<span class="kh-offer-badge confirm">Mit Bestätigung</span>';
|
||||||
|
const typeIcon = o.templateType === 'TIMELOCK'
|
||||||
|
? `<div class="kh-offer-type-icon"><span class="icon-base">🕐</span><span class="icon-lock">🔒</span></div>`
|
||||||
|
: `<div class="kh-offer-type-icon"><img src="img/card.png" class="icon-base" alt="Karten-Lock"><span class="icon-lock">🔒</span></div>`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'kh-offer-card';
|
||||||
|
div.innerHTML = `
|
||||||
|
${typeIcon}
|
||||||
|
<div class="kh-offer-body">
|
||||||
|
<div class="kh-offer-name">${esc(o.templateName || 'Unbenannt')}</div>
|
||||||
|
<div class="kh-offer-meta">
|
||||||
|
${modeBadge} ${genderTags}
|
||||||
|
<span class="kh-offer-badge">✓ ${o.acceptanceCount}× angenommen</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
async function postPinnwand() {
|
async function postPinnwand() {
|
||||||
const ta = document.getElementById('pinnwandText');
|
const ta = document.getElementById('pinnwandText');
|
||||||
const text = ta.value.trim();
|
const text = ta.value.trim();
|
||||||
|
|||||||
@@ -566,6 +566,18 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Profil bei Veröffentlichungen -->
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-row-info">
|
||||||
|
<div class="settings-row-label">Profil bei Veröffentlichungen sichtbar</div>
|
||||||
|
<div class="settings-row-desc">Dein Name wird bei veröffentlichten Lock-Vorlagen angezeigt</div>
|
||||||
|
</div>
|
||||||
|
<select id="sv-veroeffentlichungen" onchange="doSave()">
|
||||||
|
<option value="true">Ja</option>
|
||||||
|
<option value="false">Nein</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="settings-separator">
|
<hr class="settings-separator">
|
||||||
|
|
||||||
<!-- Vorschau -->
|
<!-- Vorschau -->
|
||||||
@@ -955,13 +967,14 @@
|
|||||||
if (!profRes.ok) return;
|
if (!profRes.ok) return;
|
||||||
const profile = await profRes.json();
|
const profile = await profRes.json();
|
||||||
|
|
||||||
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
|
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
|
||||||
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
|
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
|
||||||
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
|
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
|
||||||
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
|
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
|
||||||
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
|
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
|
||||||
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
|
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
|
||||||
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
|
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
|
||||||
|
setValue('sv-veroeffentlichungen', profile.profilBeiVeroeffentlichungenSichtbar ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setValue(id, value) {
|
function setValue(id, value) {
|
||||||
@@ -971,13 +984,14 @@
|
|||||||
|
|
||||||
async function doSave() {
|
async function doSave() {
|
||||||
const body = {
|
const body = {
|
||||||
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
|
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
|
||||||
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
|
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
|
||||||
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
|
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
|
||||||
sichtbarkeitFeed: document.getElementById('sv-feed').value,
|
sichtbarkeitFeed: document.getElementById('sv-feed').value,
|
||||||
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
|
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
|
||||||
sichtbarkeitXp: document.getElementById('sv-xp').value,
|
sichtbarkeitXp: document.getElementById('sv-xp').value,
|
||||||
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
|
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
|
||||||
|
profilBeiVeroeffentlichungenSichtbar: document.getElementById('sv-veroeffentlichungen').value === 'true',
|
||||||
};
|
};
|
||||||
const res = await fetch('/user/me/privacy', {
|
const res = await fetch('/user/me/privacy', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
528
xxxthegame/src/main/resources/static/entdecken-vorlagen.html
Normal file
528
xxxthegame/src/main/resources/static/entdecken-vorlagen.html
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vorlagen entdecken – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
/* ── Suche ── */
|
||||||
|
.search-bar {
|
||||||
|
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||||
|
}
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1; padding: 0.55rem 0.85rem; border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-secondary); background: var(--color-card);
|
||||||
|
color: var(--color-text); font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.search-bar button {
|
||||||
|
width: auto; padding: 0.55rem 1.2rem; font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Template-Karte ── */
|
||||||
|
.tpl-card {
|
||||||
|
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px; padding: 1rem; margin-bottom: 0.75rem;
|
||||||
|
cursor: pointer; transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.tpl-card:hover { border-color: var(--color-primary); }
|
||||||
|
.tpl-card.own-template { border-left: 3px solid #3498db; }
|
||||||
|
.tpl-card-header {
|
||||||
|
display: flex; align-items: flex-start;
|
||||||
|
justify-content: space-between; gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.tpl-icon {
|
||||||
|
width: 2.4rem; height: 2.4rem; flex-shrink: 0;
|
||||||
|
border-radius: 8px; background: var(--color-secondary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.tpl-name { font-weight: 700; font-size: 1rem; margin-bottom: 0.2rem; }
|
||||||
|
.tpl-meta { font-size: 0.78rem; color: var(--color-muted); line-height: 1.5; }
|
||||||
|
.tpl-badges {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
.tpl-badge {
|
||||||
|
font-size: 0.7rem; border-radius: 5px; padding: 0.18rem 0.55rem;
|
||||||
|
border: 1px solid var(--color-secondary); color: var(--color-muted);
|
||||||
|
background: var(--color-secondary);
|
||||||
|
}
|
||||||
|
.tpl-badge.blue { background: rgba(52,152,219,0.12); border-color: rgba(52,152,219,0.35); color: #3498db; }
|
||||||
|
.tpl-badge.green { background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.35); color: #2ecc71; }
|
||||||
|
.tpl-badge.orange { background: rgba(231,152,52,0.12); border-color: rgba(231,152,52,0.35); color: #e67e22; }
|
||||||
|
.tpl-badge.own { background: rgba(52,152,219,0.15); border-color: rgba(52,152,219,0.5); color: #3498db; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Abonnieren-Button ── */
|
||||||
|
.btn-sub {
|
||||||
|
white-space: nowrap; width: auto; padding: 0.4rem 0.9rem; font-size: 0.82rem;
|
||||||
|
font-weight: 600; border-radius: 7px; cursor: pointer; flex-shrink: 0;
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
background: none; color: var(--color-muted);
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-sub:hover:not(:disabled) {
|
||||||
|
background: rgba(52,152,219,0.12); border-color: rgba(52,152,219,0.45); color: #3498db;
|
||||||
|
}
|
||||||
|
.btn-sub.subscribed {
|
||||||
|
background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.4); color: #2ecc71;
|
||||||
|
}
|
||||||
|
.btn-sub.subscribed:hover:not(:disabled) {
|
||||||
|
background: rgba(231,76,60,0.1); border-color: rgba(231,76,60,0.35); color: #e74c3c;
|
||||||
|
}
|
||||||
|
.btn-sub:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ── Detail-Modal ── */
|
||||||
|
.detail-backdrop {
|
||||||
|
display: none; position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.65); z-index: 400;
|
||||||
|
align-items: flex-start; justify-content: center;
|
||||||
|
padding: 2rem 1rem; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.detail-backdrop.open { display: flex; }
|
||||||
|
.detail-box {
|
||||||
|
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 14px; padding: 1.75rem 1.5rem 1.5rem;
|
||||||
|
max-width: 500px; width: 100%; position: relative;
|
||||||
|
display: flex; flex-direction: column; gap: 1rem;
|
||||||
|
}
|
||||||
|
.detail-section {
|
||||||
|
background: var(--color-secondary); border-radius: 8px;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
}
|
||||||
|
.detail-section-title {
|
||||||
|
font-size: 0.72rem; font-weight: 700; color: var(--color-muted);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.detail-row {
|
||||||
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
|
font-size: 0.88rem; padding: 0.2rem 0; gap: 1rem;
|
||||||
|
}
|
||||||
|
.detail-row-label { color: var(--color-muted); flex-shrink: 0; }
|
||||||
|
.detail-row-val { color: var(--color-text); text-align: right; }
|
||||||
|
.detail-task-item {
|
||||||
|
font-size: 0.85rem; color: var(--color-text); padding: 0.3rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
.detail-task-item:last-child { border-bottom: none; }
|
||||||
|
.detail-wheel-entry {
|
||||||
|
display: inline-flex; align-items: center; gap: 0.3rem;
|
||||||
|
font-size: 0.78rem; background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary); border-radius: 5px;
|
||||||
|
padding: 0.2rem 0.55rem; margin: 0.2rem;
|
||||||
|
}
|
||||||
|
.detail-footer {
|
||||||
|
display: flex; gap: 0.75rem; justify-content: flex-end; flex-wrap: wrap;
|
||||||
|
border-top: 1px solid var(--color-secondary); padding-top: 1rem;
|
||||||
|
}
|
||||||
|
.btn-close-detail {
|
||||||
|
background: none; border: 1px solid var(--color-secondary);
|
||||||
|
color: var(--color-muted); padding: 0.5rem 1.1rem; border-radius: 7px;
|
||||||
|
cursor: pointer; font-size: 0.88rem; width: auto;
|
||||||
|
}
|
||||||
|
.btn-subscribe-detail {
|
||||||
|
padding: 0.5rem 1.25rem; border-radius: 7px; cursor: pointer;
|
||||||
|
font-size: 0.88rem; font-weight: 600; width: auto; border: none;
|
||||||
|
background: var(--color-primary); color: #fff;
|
||||||
|
}
|
||||||
|
.btn-subscribe-detail.subscribed {
|
||||||
|
background: rgba(46,204,113,0.15); border: 1px solid rgba(46,204,113,0.4); color: #2ecc71;
|
||||||
|
}
|
||||||
|
.detail-author-avatar {
|
||||||
|
width: 52px; height: 52px; border-radius: 50%;
|
||||||
|
border: 2px solid var(--color-secondary);
|
||||||
|
background: var(--color-secondary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.5rem; color: var(--color-muted);
|
||||||
|
overflow: hidden; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.detail-author-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<h2 style="margin-bottom:1.25rem;">🔍 Vorlagen entdecken</h2>
|
||||||
|
|
||||||
|
<!-- Suchleiste -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="searchInput" placeholder="Nach Namen suchen…"
|
||||||
|
onkeydown="if(event.key==='Enter') doSearch()">
|
||||||
|
<button onclick="doSearch()">Suchen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ergebnisliste -->
|
||||||
|
<div id="templateList"></div>
|
||||||
|
<div id="scrollSentinel" style="height:1px;"></div>
|
||||||
|
<p id="listLoading" style="display:none;text-align:center;color:var(--color-muted);padding:1rem;">Laden…</p>
|
||||||
|
<p id="listEmpty" style="display:none;color:var(--color-muted);">Keine öffentlichen Vorlagen gefunden.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail-Modal -->
|
||||||
|
<div class="detail-backdrop" id="detailModal" onclick="closeDetail()">
|
||||||
|
<div class="detail-box" onclick="event.stopPropagation()">
|
||||||
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;">
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:0.85rem;">
|
||||||
|
<div class="detail-author-avatar" id="detailAuthorAvatar" style="display:none;">◉</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="detailTitle" style="margin:0 0 0.25rem;font-size:1.2rem;"></h2>
|
||||||
|
<div id="detailMeta" style="font-size:0.82rem;color:var(--color-muted);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="closeDetail()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0;flex-shrink:0;">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="detailBody"></div>
|
||||||
|
|
||||||
|
<div class="detail-footer">
|
||||||
|
<button class="btn-close-detail" onclick="closeDetail()">Schließen</button>
|
||||||
|
<button class="btn-subscribe-detail" id="detailSubscribeBtn" onclick="toggleSubscribeDetail()"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/shared.js"></script>
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
<script>
|
||||||
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
|
let page = 0;
|
||||||
|
let isLastPage = false;
|
||||||
|
let isLoading = false;
|
||||||
|
let currentSearch = '';
|
||||||
|
let _detailTemplate = null;
|
||||||
|
|
||||||
|
function fmtMinutes(min) {
|
||||||
|
if (!min) return '–';
|
||||||
|
const d = Math.floor(min / 1440), h = Math.floor((min % 1440) / 60), m = min % 60;
|
||||||
|
return [d && d + 'T', h && h + 'Std', m && m + 'Min'].filter(Boolean).join(' ') || '0Min';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Laden ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadNextPage() {
|
||||||
|
if (isLoading || isLastPage) return;
|
||||||
|
isLoading = true;
|
||||||
|
document.getElementById('listLoading').style.display = '';
|
||||||
|
try {
|
||||||
|
const q = encodeURIComponent(currentSearch);
|
||||||
|
const res = await fetch(`/templates/public?page=${page}&size=10&q=${q}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
data.content.forEach(t => appendCard(t));
|
||||||
|
isLastPage = !data.hasMore;
|
||||||
|
page = data.page + 1;
|
||||||
|
if (page === 1 && data.content.length === 0) {
|
||||||
|
document.getElementById('listEmpty').style.display = '';
|
||||||
|
}
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
finally {
|
||||||
|
isLoading = false;
|
||||||
|
document.getElementById('listLoading').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetList() {
|
||||||
|
page = 0; isLastPage = false; isLoading = false;
|
||||||
|
document.getElementById('templateList').innerHTML = '';
|
||||||
|
document.getElementById('listEmpty').style.display = 'none';
|
||||||
|
loadNextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch() {
|
||||||
|
currentSearch = document.getElementById('searchInput').value.trim();
|
||||||
|
resetList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Karte ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function appendCard(t) {
|
||||||
|
const list = document.getElementById('templateList');
|
||||||
|
const isCard = t.lockType === 'CARDLOCK';
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'tpl-card' + (t.isOwnTemplate ? ' own-template' : '');
|
||||||
|
card.dataset.templateId = t.templateId;
|
||||||
|
|
||||||
|
const metaParts = [
|
||||||
|
isCard ? '🃏 Karten-Lock' : '⏱ Zeit-Lock',
|
||||||
|
t.authorName ? 'von ' + esc(t.authorName) : null,
|
||||||
|
t.subscriberCount + ' Abo(s)',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const badges = buildBadges(t);
|
||||||
|
const subBtnCls = t.isOwnTemplate ? '' : (t.isSubscribed ? 'subscribed' : '');
|
||||||
|
const subBtnLabel = t.isOwnTemplate ? 'Eigene' : (t.isSubscribed ? '✓ Abonniert' : '+ Abonnieren');
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="tpl-card-header">
|
||||||
|
<div class="tpl-icon">${isCard ? '🃏' : '⏱'}</div>
|
||||||
|
<div style="flex:1;min-width:0;">
|
||||||
|
<div class="tpl-name">${esc(t.name || 'Ohne Namen')}</div>
|
||||||
|
<div class="tpl-meta">${metaParts.join(' · ')}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-sub ${subBtnCls}" ${t.isOwnTemplate ? 'disabled' : ''}
|
||||||
|
onclick="event.stopPropagation();toggleSubscribe('${t.templateId}',this)">
|
||||||
|
${subBtnLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tpl-badges">${badges}</div>`;
|
||||||
|
|
||||||
|
card.addEventListener('click', () => openDetail(t));
|
||||||
|
list.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBadges(t) {
|
||||||
|
const b = [];
|
||||||
|
if (t.lockType === 'TIMELOCK') {
|
||||||
|
if (t.minTimeInMinutes || t.maxTimeInMinutes) {
|
||||||
|
const min = fmtMinutes(t.minTimeInMinutes), max = fmtMinutes(t.maxTimeInMinutes);
|
||||||
|
b.push(`<span class="tpl-badge blue">⏱ ${min} – ${max}</span>`);
|
||||||
|
}
|
||||||
|
if (t.spinningWheelEntries && t.spinningWheelEntries.length)
|
||||||
|
b.push(`<span class="tpl-badge orange">🎡 Glücksrad (${t.spinningWheelEntries.length})</span>`);
|
||||||
|
if (t.penaltyType)
|
||||||
|
b.push(`<span class="tpl-badge orange">⚠ Strafe</span>`);
|
||||||
|
}
|
||||||
|
if (t.taskCount > 0)
|
||||||
|
b.push(`<span class="tpl-badge">🎯 ${t.taskCount} Aufgabe(n)</span>`);
|
||||||
|
if (t.hygieneEnabled)
|
||||||
|
b.push(`<span class="tpl-badge">🚿 Hygiene</span>`);
|
||||||
|
if (t.requiresVerification)
|
||||||
|
b.push(`<span class="tpl-badge">📷 Verifikation</span>`);
|
||||||
|
if (t.isOwnTemplate)
|
||||||
|
b.push(`<span class="tpl-badge own">Meine Vorlage</span>`);
|
||||||
|
return b.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Abonnieren (Listenansicht) ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function toggleSubscribe(id, btn) {
|
||||||
|
const isSubscribed = btn.classList.contains('subscribed');
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||||
|
const res = await fetch(`/templates/${id}/subscribe`, { method });
|
||||||
|
if (res.ok || res.status === 204) {
|
||||||
|
if (isSubscribed) {
|
||||||
|
btn.classList.remove('subscribed');
|
||||||
|
btn.textContent = '+ Abonnieren';
|
||||||
|
// Update card data
|
||||||
|
const card = btn.closest('.tpl-card');
|
||||||
|
updateCardSubscriberCount(card, -1);
|
||||||
|
} else {
|
||||||
|
btn.classList.add('subscribed');
|
||||||
|
btn.textContent = '✓ Abonniert';
|
||||||
|
const card = btn.closest('.tpl-card');
|
||||||
|
updateCardSubscriberCount(card, +1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCardSubscriberCount(card, delta) {
|
||||||
|
// Update the meta text - find the "X Abo(s)" part
|
||||||
|
const meta = card.querySelector('.tpl-meta');
|
||||||
|
if (!meta) return;
|
||||||
|
meta.innerHTML = meta.innerHTML.replace(/(\d+) Abo\(s\)/, (_, n) => `${Math.max(0, parseInt(n) + delta)} Abo(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail-Modal ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function openDetail(t) {
|
||||||
|
_detailTemplate = t;
|
||||||
|
document.getElementById('detailTitle').textContent = t.name || 'Ohne Namen';
|
||||||
|
|
||||||
|
const avatarEl = document.getElementById('detailAuthorAvatar');
|
||||||
|
if (t.authorProfilePicture) {
|
||||||
|
avatarEl.innerHTML = `<img src="data:image/png;base64,${t.authorProfilePicture}" alt="${esc(t.authorName || '')}">`;
|
||||||
|
avatarEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
avatarEl.innerHTML = '◉';
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaParts = [
|
||||||
|
t.lockType === 'CARDLOCK' ? '🃏 Karten-Lock' : '⏱ Zeit-Lock',
|
||||||
|
t.authorName ? 'von ' + t.authorName : null,
|
||||||
|
t.subscriberCount + ' Abonnent(en)',
|
||||||
|
].filter(Boolean);
|
||||||
|
document.getElementById('detailMeta').textContent = metaParts.join(' · ');
|
||||||
|
|
||||||
|
document.getElementById('detailBody').innerHTML = buildDetailBody(t);
|
||||||
|
|
||||||
|
const btn = document.getElementById('detailSubscribeBtn');
|
||||||
|
if (t.isOwnTemplate) {
|
||||||
|
btn.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
btn.style.display = '';
|
||||||
|
btn.className = 'btn-subscribe-detail' + (t.isSubscribed ? ' subscribed' : '');
|
||||||
|
btn.textContent = t.isSubscribed ? '✓ Abonniert' : '+ Abonnieren';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('detailModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
document.getElementById('detailModal').classList.remove('open');
|
||||||
|
_detailTemplate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSubscribeDetail() {
|
||||||
|
if (!_detailTemplate) return;
|
||||||
|
const t = _detailTemplate;
|
||||||
|
const btn = document.getElementById('detailSubscribeBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
const isSubscribed = t.isSubscribed;
|
||||||
|
try {
|
||||||
|
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||||
|
const res = await fetch(`/templates/${t.templateId}/subscribe`, { method });
|
||||||
|
if (res.ok || res.status === 204) {
|
||||||
|
t.isSubscribed = !isSubscribed;
|
||||||
|
t.subscriberCount = Math.max(0, (t.subscriberCount || 0) + (isSubscribed ? -1 : 1));
|
||||||
|
if (isSubscribed) {
|
||||||
|
btn.className = 'btn-subscribe-detail';
|
||||||
|
btn.textContent = '+ Abonnieren';
|
||||||
|
} else {
|
||||||
|
btn.className = 'btn-subscribe-detail subscribed';
|
||||||
|
btn.textContent = '✓ Abonniert';
|
||||||
|
}
|
||||||
|
// Update card in list
|
||||||
|
const card = document.querySelector(`.tpl-card[data-template-id="${t.templateId}"]`);
|
||||||
|
if (card) {
|
||||||
|
const subBtn = card.querySelector('.btn-sub');
|
||||||
|
if (subBtn) {
|
||||||
|
if (isSubscribed) { subBtn.classList.remove('subscribed'); subBtn.textContent = '+ Abonnieren'; }
|
||||||
|
else { subBtn.classList.add('subscribed'); subBtn.textContent = '✓ Abonniert'; }
|
||||||
|
}
|
||||||
|
updateCardSubscriberCount(card, isSubscribed ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail-Body aufbauen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function buildDetailBody(t) {
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
if (t.lockType === 'TIMELOCK') {
|
||||||
|
sections.push(buildSection('⏱ Zeit-Einstellungen', [
|
||||||
|
['Mindestdauer', fmtMinutes(t.minTimeInMinutes)],
|
||||||
|
['Maximaldauer', fmtMinutes(t.maxTimeInMinutes)],
|
||||||
|
['Endzeit sichtbar', t.endTimeVisible ? 'Ja' : 'Nein'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
if (t.spinningWheelEntries && t.spinningWheelEntries.length) {
|
||||||
|
const WHEEL_LABELS = {
|
||||||
|
ADD_TIME: '+ Zeit', REMOVE_TIME: '− Zeit', FREEZE_TIME: '❄ Einfrieren für',
|
||||||
|
FREEZE: '🧊 Einfrieren (∞)', UNFREEZE: '🌊 Auftauen', TASK: '🎯 Aufgabe', TEXT: '💬 Text',
|
||||||
|
};
|
||||||
|
const entries = t.spinningWheelEntries.map(e => {
|
||||||
|
const label = WHEEL_LABELS[e.type] || e.type;
|
||||||
|
const extra = e.intVal ? ' ' + fmtMinutes(e.intVal) : (e.stringVal ? ' «' + e.stringVal + '»' : '');
|
||||||
|
return `<span class="detail-wheel-entry">${label}${extra}</span>`;
|
||||||
|
}).join('');
|
||||||
|
sections.push(`<div class="detail-section">
|
||||||
|
<div class="detail-section-title">🎡 Glücksrad (${t.spinningWheelEntries.length} Einträge${t.spinsEveryMinutes ? ', alle ' + fmtMinutes(t.spinsEveryMinutes) : ''})</div>
|
||||||
|
<div>${entries}</div>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.penaltyType) {
|
||||||
|
const penaltyLabels = { ADD: 'Zeit hinzufügen', FREEZE: 'Einfrieren', PILLORY: 'Pranger' };
|
||||||
|
sections.push(buildSection('⚠ Strafmaß', [
|
||||||
|
['Typ', penaltyLabels[t.penaltyType] || t.penaltyType],
|
||||||
|
['Wert', t.penaltyValue ? fmtMinutes(t.penaltyValue) : '–'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.taskEveryMinutes || t.minTasksPerDay) {
|
||||||
|
sections.push(buildSection('🎯 Aufgaben-Timing', [
|
||||||
|
['Intervall', t.taskEveryMinutes ? fmtMinutes(t.taskEveryMinutes) : '–'],
|
||||||
|
['Min./Tag', t.minTasksPerDay ? t.minTasksPerDay + ' Aufgabe(n)' : '–'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.lockType === 'CARDLOCK') {
|
||||||
|
const rows = [];
|
||||||
|
const allKeys = new Set([
|
||||||
|
...Object.keys(t.cardCountsMin || {}),
|
||||||
|
...Object.keys(t.cardCountsMax || {}),
|
||||||
|
]);
|
||||||
|
allKeys.forEach(k => {
|
||||||
|
const mn = (t.cardCountsMin || {})[k] ?? 0;
|
||||||
|
const mx = (t.cardCountsMax || {})[k] ?? 0;
|
||||||
|
if (mn > 0 || mx > 0) rows.push([k, `${mn} – ${mx}`]);
|
||||||
|
});
|
||||||
|
if (rows.length)
|
||||||
|
sections.push(buildSection('🃏 Karten', rows));
|
||||||
|
|
||||||
|
sections.push(buildSection('⚙ Karten-Einstellungen', [
|
||||||
|
['Zieh-Intervall', t.pickEveryMinute ? fmtMinutes(t.pickEveryMinute) : '–'],
|
||||||
|
['Picks kumulieren', t.accumulatePicks ? 'Ja' : 'Nein'],
|
||||||
|
['Verbl. Karten zeigen', t.showRemainingCards ? 'Ja' : 'Nein'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemeinsame Einstellungen
|
||||||
|
sections.push(buildSection('⚙ Allgemein', [
|
||||||
|
['Hygiene-Öffnung', t.hygieneEnabled ? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen` : 'Keine'],
|
||||||
|
['Verifikation', t.requiresVerification ? 'Erforderlich' : 'Keine'],
|
||||||
|
['Aufgaben-Modus', t.taskMode === 'KEYHOLDER' ? 'Keyholder' : t.taskMode === 'COMMUNITY' ? 'Community' : 'Zufällig'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
if (t.tasks && t.tasks.length) {
|
||||||
|
const taskItems = t.tasks.map(task => {
|
||||||
|
const dur = task.durationMinutes ? ` <span style="color:var(--color-muted);font-size:0.8rem;">(${fmtMinutes(task.durationMinutes)})</span>` : '';
|
||||||
|
const desc = task.description ? `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.1rem;">${esc(task.description)}</div>` : '';
|
||||||
|
return `<div class="detail-task-item">${esc(task.title || task.name || '–')}${dur}${desc}</div>`;
|
||||||
|
}).join('');
|
||||||
|
sections.push(`<div class="detail-section">
|
||||||
|
<div class="detail-section-title">🎯 Aufgaben (${t.tasks.length})</div>
|
||||||
|
${taskItems}
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSection(title, rows) {
|
||||||
|
const rowsHtml = rows.map(([label, val]) =>
|
||||||
|
`<div class="detail-row">
|
||||||
|
<span class="detail-row-label">${label}</span>
|
||||||
|
<span class="detail-row-val">${val}</span>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
return `<div class="detail-section">
|
||||||
|
<div class="detail-section-title">${title}</div>
|
||||||
|
${rowsHtml}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Infinite Scroll ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
if (entries[0].isIntersecting) loadNextPage();
|
||||||
|
}, { rootMargin: '200px' });
|
||||||
|
observer.observe(document.getElementById('scrollSentinel'));
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') closeDetail();
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||||||
|
if (!user) { window.location.href = '/login.html'; return; }
|
||||||
|
loadNextPage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -66,7 +66,7 @@ const CARD_DEFS = [
|
|||||||
id: 'CUM',
|
id: 'CUM',
|
||||||
img: '/img/card_cum.png',
|
img: '/img/card_cum.png',
|
||||||
name: 'Cum',
|
name: 'Cum',
|
||||||
desc: 'Spezielle Karte.',
|
desc: 'Du wirst entsperrt, nutze diese Entsperrung um zu kommen. Je länger du brauchst, desto schlimmer.',
|
||||||
defMin: 0,
|
defMin: 0,
|
||||||
defMax: 0,
|
defMax: 0,
|
||||||
},
|
},
|
||||||
@@ -74,7 +74,7 @@ const CARD_DEFS = [
|
|||||||
id: 'CUM_IN_CAGE',
|
id: 'CUM_IN_CAGE',
|
||||||
img: '/img/card_cum_caged.png',
|
img: '/img/card_cum_caged.png',
|
||||||
name: 'Cum in Cage',
|
name: 'Cum in Cage',
|
||||||
desc: 'Spezielle Karte.',
|
desc: 'Komme in deinem Keuschheitsgürtel, wie du es anstellst ist deine Sache.',
|
||||||
defMin: 0,
|
defMin: 0,
|
||||||
defMax: 0,
|
defMax: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
{ href: '/neulock.html', icon: I('NEW_LOCK'), label: 'Neues Lock', id: 'navChastityNeu' },
|
{ href: '/neulock.html', icon: I('NEW_LOCK'), label: 'Neues Lock', id: 'navChastityNeu' },
|
||||||
{ href: '#', icon: I('ACTIVE_LOCK'), label: 'Aktives Lock', id: 'navChastityAktiv' },
|
{ href: '#', icon: I('ACTIVE_LOCK'), label: 'Aktives Lock', id: 'navChastityAktiv' },
|
||||||
{ href: '/communityvotes.html', icon: I('VOTES'), label: 'Community Votes' },
|
{ href: '/communityvotes.html', icon: I('VOTES'), label: 'Community Votes' },
|
||||||
{ href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Locks' },
|
{ href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Vorlagen' },
|
||||||
|
{ href: '/entdecken-vorlagen.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||||
|
{ href: '/keyholder-finden.html', icon: I('FRIENDS'), label: 'Keyholder finden' },
|
||||||
{ href: '/keyholder.html', icon: I('KEY'), label: 'Keyholder' },
|
{ href: '/keyholder.html', icon: I('KEY'), label: 'Keyholder' },
|
||||||
{ href: '/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' },
|
{ href: '/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -277,24 +277,25 @@
|
|||||||
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Benachrichtigungen.</div>'; return; }
|
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Benachrichtigungen.</div>'; return; }
|
||||||
const notifs = await res.json();
|
const notifs = await res.json();
|
||||||
if (!notifs.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
|
if (!notifs.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
|
||||||
body.innerHTML = `<div style="padding:0.3rem 1rem;text-align:right;">
|
body.innerHTML = '';
|
||||||
<button onclick="window.__topbarMarkAllRead()" class="topbar-mark-all-btn">Alle gelesen</button>
|
|
||||||
</div>`;
|
|
||||||
notifs.forEach(n => {
|
notifs.forEach(n => {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
const tag = n.targetUrl ? 'a' : 'div';
|
const tag = n.targetUrl ? 'a' : 'div';
|
||||||
const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : '';
|
const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : '';
|
||||||
const unread = !n.read;
|
const av = n.senderAvatar
|
||||||
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item${unread ? ' topbar-notif-item--unread' : ''}"
|
? `<img src="data:image/png;base64,${esc(n.senderAvatar)}" class="topbar-item-avatar" alt="">`
|
||||||
onclick="window.__topbarMarkNotifRead('${esc(n.id)}')">
|
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||||
${unread ? '<span class="topbar-notif-dot"></span>' : '<span style="width:7px;flex-shrink:0;"></span>'}
|
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item">
|
||||||
|
${av}
|
||||||
<div class="topbar-panel-item-body">
|
<div class="topbar-panel-item-body">
|
||||||
<div style="font-size:0.85rem;line-height:1.4;${unread ? 'font-weight:600;' : ''}">${esc(n.text)}</div>
|
<div style="font-size:0.85rem;line-height:1.4;">${esc(n.text)}</div>
|
||||||
<div class="topbar-panel-item-sub">${n.sentAt ? new Date(n.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'}) : ''}</div>
|
<div class="topbar-panel-item-sub">${n.sentAt ? new Date(n.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'}) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
</${tag}>`;
|
</${tag}>`;
|
||||||
body.appendChild(el.firstElementChild);
|
body.appendChild(el.firstElementChild);
|
||||||
});
|
});
|
||||||
|
// Alle als gelesen markieren
|
||||||
|
fetch('/notifications/read-all', { method: 'POST' }).then(() => setTopbarBadge('notif', 0)).catch(() => {});
|
||||||
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
|
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
530
xxxthegame/src/main/resources/static/keyholder-finden.html
Normal file
530
xxxthegame/src/main/resources/static/keyholder-finden.html
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Keyholder finden – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
.offer-card {
|
||||||
|
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px; padding: 1rem; margin-bottom: 0.75rem;
|
||||||
|
display: flex; align-items: center; gap: 0.85rem;
|
||||||
|
}
|
||||||
|
.offer-avatar {
|
||||||
|
width: 48px; height: 48px; border-radius: 50%;
|
||||||
|
background: var(--color-secondary); border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.3rem; flex-shrink: 0; overflow: hidden;
|
||||||
|
}
|
||||||
|
.offer-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.offer-body { flex: 1; min-width: 0; }
|
||||||
|
.offer-name { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.2rem;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.offer-sub { font-size: 0.78rem; color: var(--color-muted); margin-bottom: 0.3rem; }
|
||||||
|
.offer-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
||||||
|
.offer-badge {
|
||||||
|
display: inline-block; font-size: 0.72rem; padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 4px; background: rgba(255,255,255,0.07); border: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.offer-badge.direct { background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.3); color: #2ecc71; }
|
||||||
|
.offer-badge.confirm { background: rgba(230,126,34,0.12); border-color: rgba(230,126,34,0.3); color: #e67e22; }
|
||||||
|
.btn-join {
|
||||||
|
background: var(--color-primary); border: none; color: #fff;
|
||||||
|
border-radius: 7px; padding: 0.4rem 1rem; font-size: 0.85rem;
|
||||||
|
font-weight: 600; cursor: pointer; flex-shrink: 0; width: auto;
|
||||||
|
}
|
||||||
|
.btn-join:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
|
||||||
|
/* Klickbarer Card-Bereich */
|
||||||
|
.offer-card-clickable { cursor: pointer; }
|
||||||
|
.offer-card-clickable:hover { background: var(--color-secondary); }
|
||||||
|
|
||||||
|
/* Detail-Dialog */
|
||||||
|
.detail-backdrop {
|
||||||
|
display: none; position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.65); z-index: 500;
|
||||||
|
align-items: flex-start; justify-content: center;
|
||||||
|
overflow-y: auto; padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
.detail-backdrop.open { display: flex; }
|
||||||
|
.detail-box {
|
||||||
|
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 14px; padding: 1.5rem; max-width: 520px; width: 100%;
|
||||||
|
display: flex; flex-direction: column; gap: 1rem; position: relative;
|
||||||
|
}
|
||||||
|
.detail-section { margin-bottom: 0.25rem; }
|
||||||
|
.detail-section-title {
|
||||||
|
font-size: 0.72rem; font-weight: 700; color: var(--color-primary);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.detail-row {
|
||||||
|
display: flex; justify-content: space-between; gap: 1rem;
|
||||||
|
padding: 0.25rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
.detail-row:last-child { border-bottom: none; }
|
||||||
|
.detail-row-label { color: var(--color-muted); flex-shrink: 0; }
|
||||||
|
.detail-row-val { color: var(--color-text); text-align: right; }
|
||||||
|
.detail-task-item {
|
||||||
|
font-size: 0.88rem; padding: 0.35rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.detail-task-item:last-child { border-bottom: none; }
|
||||||
|
.detail-wheel-entry {
|
||||||
|
display: inline-block; font-size: 0.8rem; padding: 0.15rem 0.5rem;
|
||||||
|
border-radius: 4px; background: rgba(255,255,255,0.07);
|
||||||
|
border: 1px solid var(--color-secondary); margin: 0.15rem 0.2rem 0.15rem 0;
|
||||||
|
}
|
||||||
|
.detail-footer {
|
||||||
|
display: flex; gap: 0.6rem; justify-content: flex-end;
|
||||||
|
border-top: 1px solid var(--color-secondary); padding-top: 1rem; margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.btn-close-detail {
|
||||||
|
background: none; border: 1px solid var(--color-secondary);
|
||||||
|
color: var(--color-muted); padding: 0.5rem 1.1rem;
|
||||||
|
border-radius: 7px; cursor: pointer; font-size: 0.88rem; width: auto;
|
||||||
|
}
|
||||||
|
.btn-join-detail {
|
||||||
|
background: var(--color-primary); border: none; color: #fff;
|
||||||
|
border-radius: 7px; padding: 0.5rem 1.25rem; font-size: 0.88rem;
|
||||||
|
font-weight: 600; cursor: pointer; width: auto;
|
||||||
|
}
|
||||||
|
.btn-join-detail:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
.detail-author-avatar {
|
||||||
|
width: 52px; height: 52px; border-radius: 50%;
|
||||||
|
background: var(--color-secondary); border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.detail-author-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
|
||||||
|
/* Join-Dialog */
|
||||||
|
.join-modal-backdrop {
|
||||||
|
display: none; position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.65); z-index: 500;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.join-modal-backdrop.open { display: flex; }
|
||||||
|
.join-modal-box {
|
||||||
|
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 14px; padding: 1.5rem; max-width: 400px; width: 92%;
|
||||||
|
display: flex; flex-direction: column; gap: 1rem; position: relative;
|
||||||
|
}
|
||||||
|
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||||
|
.form-label { font-size: 0.72rem; font-weight: 700; color: var(--color-primary);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.06em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
<h2 style="margin-bottom:1.25rem;">🔍 Keyholder finden</h2>
|
||||||
|
<p style="font-size:0.88rem;color:var(--color-muted);margin-bottom:1.25rem;line-height:1.5;">
|
||||||
|
Hier findest du Nutzer*innen, die sich als Keyholder für ein bestimmtes Lock-Template anbieten.
|
||||||
|
Die beliebtesten Angebote erscheinen ganz oben.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="offerList"></div>
|
||||||
|
<p id="listEmpty" style="display:none;color:var(--color-muted);">Keine Keyholder-Angebote gefunden.</p>
|
||||||
|
<p id="listLoading" style="color:var(--color-muted);">Wird geladen…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail-Dialog -->
|
||||||
|
<div class="detail-backdrop" id="detailModal" onclick="closeDetail()">
|
||||||
|
<div class="detail-box" onclick="event.stopPropagation()">
|
||||||
|
<button onclick="closeDetail()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;">✕</button>
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:0.85rem;">
|
||||||
|
<div class="detail-author-avatar" id="detailAvatar" style="display:none;">◉</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="detailTitle" style="margin:0 0 0.25rem;font-size:1.2rem;"></h2>
|
||||||
|
<div id="detailMeta" style="font-size:0.82rem;color:var(--color-muted);"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="detailBody"></div>
|
||||||
|
<div class="detail-footer">
|
||||||
|
<button class="btn-close-detail" onclick="closeDetail()">Schließen</button>
|
||||||
|
<button class="btn-join-detail" id="detailJoinBtn" onclick="detailJoin()">🔒 Beitreten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Join-Dialog -->
|
||||||
|
<div class="join-modal-backdrop" id="joinModal">
|
||||||
|
<div class="join-modal-box">
|
||||||
|
<button onclick="closeJoinModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;">✕</button>
|
||||||
|
<h3 style="margin:0;font-size:1.05rem;">🔒 Angebot annehmen</h3>
|
||||||
|
<p id="joinModalDesc" style="margin:0;font-size:0.85rem;color:var(--color-muted);line-height:1.5;"></p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-label">Schloss-Steuerung</div>
|
||||||
|
<select id="joinControllType" style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;">
|
||||||
|
<option value="">– Bitte wählen –</option>
|
||||||
|
<option value="UNLOCK_CODE">🔢 Entsperrcode (Standard)</option>
|
||||||
|
<option value="TRUST">🤝 Trust (kein Code)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="codeLenGroup">
|
||||||
|
<div class="form-label">Code-Länge</div>
|
||||||
|
<input type="number" id="joinCodeLen" value="5" min="1" max="10"
|
||||||
|
style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;width:100%;box-sizing:border-box;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="joinError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.25rem;">
|
||||||
|
<button onclick="closeJoinModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
|
||||||
|
<button id="joinConfirmBtn" onclick="confirmJoin()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;">Beitreten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ergebnis-Dialog (nach erfolgreichem Join) -->
|
||||||
|
<div class="join-modal-backdrop" id="joinResultModal">
|
||||||
|
<div class="join-modal-box" style="align-items:center;text-align:center;">
|
||||||
|
<div style="font-size:2.5rem;line-height:1;" id="joinResultIcon">🔒</div>
|
||||||
|
<h3 style="margin:0;" id="joinResultTitle"></h3>
|
||||||
|
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);line-height:1.5;" id="joinResultText"></p>
|
||||||
|
<div id="joinResultCode" style="display:none;font-family:monospace;font-size:1.6rem;font-weight:700;letter-spacing:0.18em;padding:0.6rem 1.25rem;background:rgba(255,255,255,0.06);border-radius:8px;"></div>
|
||||||
|
<div style="display:flex;gap:0.6rem;justify-content:center;margin-top:0.5rem;flex-wrap:wrap;">
|
||||||
|
<button onclick="closeJoinResultModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Schließen</button>
|
||||||
|
<button id="btnGoToLock" onclick="goToActiveLock()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;display:none;">Zum aktiven Lock</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/shared.js"></script>
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
<script>
|
||||||
|
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
|
const GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
|
||||||
|
|
||||||
|
let _joinOfferId = null;
|
||||||
|
let _lastJoinOfferId = null;
|
||||||
|
let _joinLockId = null;
|
||||||
|
let _detailOfferId = null;
|
||||||
|
let _detailOffer = null;
|
||||||
|
let _allOffers = [];
|
||||||
|
|
||||||
|
// ── Laden ──────────────────────────────────────────────────────────────────
|
||||||
|
async function loadOffers() {
|
||||||
|
const res = await fetch('/keyholder-offers/public');
|
||||||
|
document.getElementById('listLoading').style.display = 'none';
|
||||||
|
if (!res.ok) return;
|
||||||
|
_allOffers = await res.json();
|
||||||
|
const list = document.getElementById('offerList');
|
||||||
|
if (_allOffers.length === 0) { document.getElementById('listEmpty').style.display = ''; return; }
|
||||||
|
_allOffers.forEach(o => list.appendChild(buildCard(o)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCard(o) {
|
||||||
|
const av = o.offererProfilePic
|
||||||
|
? `<div class="offer-avatar"><img src="data:image/png;base64,${o.offererProfilePic}" alt=""></div>`
|
||||||
|
: `<div class="offer-avatar">👤</div>`;
|
||||||
|
|
||||||
|
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
|
||||||
|
? o.targetGenders.map(g => `<span class="offer-badge">${esc(GENDER_LABELS[g] || g)}</span>`).join('')
|
||||||
|
: '<span class="offer-badge">Alle</span>';
|
||||||
|
|
||||||
|
const modeBadge = o.directStart
|
||||||
|
? '<span class="offer-badge direct">Direktstart</span>'
|
||||||
|
: '<span class="offer-badge confirm">Mit Bestätigung</span>';
|
||||||
|
|
||||||
|
const typeBadge = o.templateType === 'TIMELOCK'
|
||||||
|
? '<span class="offer-badge">⏱ Zeit-Lock</span>'
|
||||||
|
: '<span class="offer-badge">🃏 Karten-Lock</span>';
|
||||||
|
|
||||||
|
const authorLine = o.offererName
|
||||||
|
? `von ${esc(o.offererName)} · `
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const joinBtn = o.isOwn
|
||||||
|
? `<button class="btn-join" disabled title="Eigenes Angebot">Eigenes</button>`
|
||||||
|
: `<button class="btn-join" onclick="openJoinModal('${o.id}', event)">Beitreten</button>`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'offer-card';
|
||||||
|
div.dataset.offerId = o.id;
|
||||||
|
div.innerHTML = `
|
||||||
|
${av}
|
||||||
|
<div class="offer-body offer-card-clickable" onclick="openDetail('${o.id}')">
|
||||||
|
<div class="offer-name">${esc(o.templateName || 'Unbenannt')}</div>
|
||||||
|
<div class="offer-sub">${authorLine}✓ ${o.acceptanceCount}× angenommen</div>
|
||||||
|
<div class="offer-tags">${typeBadge} ${modeBadge} ${genderTags}</div>
|
||||||
|
</div>
|
||||||
|
${joinBtn}`;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Join-Dialog ────────────────────────────────────────────────────────────
|
||||||
|
function openJoinModal(offerId, e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
_joinOfferId = offerId;
|
||||||
|
const card = document.querySelector(`[data-offer-id="${offerId}"]`);
|
||||||
|
const name = card ? card.querySelector('.offer-name')?.textContent : 'dieses Lock';
|
||||||
|
const direct = card?.querySelector('.offer-badge.direct') != null;
|
||||||
|
|
||||||
|
document.getElementById('joinModalDesc').textContent = direct
|
||||||
|
? `Das Lock „${name}" wird sofort für dich gestartet. Bitte wähle deine bevorzugte Schloss-Steuerung.`
|
||||||
|
: `Du sendest eine Einladung an den Keyholder für das Lock „${name}". Nach Annahme kannst du loslegen.`;
|
||||||
|
|
||||||
|
document.getElementById('joinError').style.display = 'none';
|
||||||
|
document.getElementById('joinControllType').value = '';
|
||||||
|
document.getElementById('joinCodeLen').value = '5';
|
||||||
|
document.getElementById('joinConfirmBtn').disabled = true;
|
||||||
|
updateCodeLenVisibility();
|
||||||
|
document.getElementById('joinModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeJoinModal() {
|
||||||
|
document.getElementById('joinModal').classList.remove('open');
|
||||||
|
_joinOfferId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('joinControllType').addEventListener('change', function() {
|
||||||
|
updateCodeLenVisibility();
|
||||||
|
document.getElementById('joinConfirmBtn').disabled = !this.value;
|
||||||
|
});
|
||||||
|
function updateCodeLenVisibility() {
|
||||||
|
const val = document.getElementById('joinControllType').value;
|
||||||
|
document.getElementById('codeLenGroup').style.display = val === 'TRUST' ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmJoin() {
|
||||||
|
if (!_joinOfferId) return;
|
||||||
|
const controllType = document.getElementById('joinControllType').value;
|
||||||
|
if (!controllType) return;
|
||||||
|
const btn = document.getElementById('joinConfirmBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
const unlockCodeLength = parseInt(document.getElementById('joinCodeLen').value) || 5;
|
||||||
|
|
||||||
|
const res = await fetch(`/keyholder-offers/${_joinOfferId}/join`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ controllType, unlockCodeLength })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const d = await res.json().catch(() => ({}));
|
||||||
|
let msg = 'Fehler beim Beitreten.';
|
||||||
|
if (d.error === 'active_lock_exists') msg = 'Du hast bereits ein aktives Lock.';
|
||||||
|
else if (d.error === 'own_offer') msg = 'Du kannst nicht deinem eigenen Angebot beitreten.';
|
||||||
|
else if (d.error === 'template_gone') msg = 'Die Vorlage existiert nicht mehr.';
|
||||||
|
document.getElementById('joinError').textContent = msg;
|
||||||
|
document.getElementById('joinError').style.display = '';
|
||||||
|
btn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
_lastJoinOfferId = _joinOfferId;
|
||||||
|
closeJoinModal();
|
||||||
|
showJoinResult(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showJoinResult(data) {
|
||||||
|
_joinLockId = data.lockId;
|
||||||
|
const direct = !data.invitationSent;
|
||||||
|
|
||||||
|
document.getElementById('joinResultIcon').textContent = direct ? '🔒' : '✉️';
|
||||||
|
document.getElementById('joinResultTitle').textContent = direct ? 'Lock gestartet!' : 'Einladung gesendet';
|
||||||
|
document.getElementById('btnGoToLock').style.display = direct ? '' : 'none';
|
||||||
|
|
||||||
|
if (direct && data.unlockCode) {
|
||||||
|
document.getElementById('joinResultText').textContent = 'Dein aktueller Entsperrcode:';
|
||||||
|
document.getElementById('joinResultCode').textContent = data.unlockCode;
|
||||||
|
document.getElementById('joinResultCode').style.display = '';
|
||||||
|
} else if (direct) {
|
||||||
|
document.getElementById('joinResultText').textContent = 'Das Lock wurde erfolgreich gestartet.';
|
||||||
|
document.getElementById('joinResultCode').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
document.getElementById('joinResultText').textContent =
|
||||||
|
'Die Einladung wurde an den Keyholder gesendet. Sobald dieser annimmt, startet das Lock.';
|
||||||
|
document.getElementById('joinResultCode').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('joinResultModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeJoinResultModal() {
|
||||||
|
document.getElementById('joinResultModal').classList.remove('open');
|
||||||
|
_joinLockId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToActiveLock() {
|
||||||
|
if (!_joinLockId) return;
|
||||||
|
const isTimelock = _allOffers.find(o => o.id === _lastJoinOfferId)?.templateType === 'TIMELOCK';
|
||||||
|
const page = isTimelock ? '/activetimelock.html' : '/activelock.html';
|
||||||
|
window.location.href = page + '?lockId=' + _joinLockId;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeDetail();
|
||||||
|
closeJoinModal();
|
||||||
|
closeJoinResultModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Detail-Dialog ──────────────────────────────────────────────────────────
|
||||||
|
async function openDetail(offerId) {
|
||||||
|
_detailOfferId = offerId;
|
||||||
|
const card = document.querySelector(`[data-offer-id="${offerId}"]`);
|
||||||
|
|
||||||
|
// Offer-Objekt aus den geladenen Daten holen
|
||||||
|
_detailOffer = _allOffers.find(o => o.id === offerId);
|
||||||
|
if (!_detailOffer) return;
|
||||||
|
|
||||||
|
// Autoren-Avatar
|
||||||
|
const avatarEl = document.getElementById('detailAvatar');
|
||||||
|
if (_detailOffer.offererProfilePic) {
|
||||||
|
avatarEl.innerHTML = `<img src="data:image/png;base64,${_detailOffer.offererProfilePic}" alt="">`;
|
||||||
|
avatarEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
avatarEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeTxt = _detailOffer.templateType === 'TIMELOCK' ? '⏱ Zeit-Lock' : '🃏 Karten-Lock';
|
||||||
|
const modeTxt = _detailOffer.directStart ? 'Direktstart' : 'Mit Bestätigung';
|
||||||
|
const authorTxt = _detailOffer.offererName ? ' · von ' + _detailOffer.offererName : '';
|
||||||
|
document.getElementById('detailTitle').textContent = _detailOffer.templateName || 'Unbenannt';
|
||||||
|
document.getElementById('detailMeta').textContent = typeTxt + ' · ' + modeTxt + authorTxt;
|
||||||
|
|
||||||
|
// Join-Button ein/ausblenden
|
||||||
|
const joinBtn = document.getElementById('detailJoinBtn');
|
||||||
|
joinBtn.style.display = _detailOffer.isOwn ? 'none' : '';
|
||||||
|
|
||||||
|
// Template-Details laden
|
||||||
|
document.getElementById('detailBody').innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
|
||||||
|
document.getElementById('detailModal').classList.add('open');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/templates/' + _detailOffer.templateId + '/public');
|
||||||
|
if (res.ok) {
|
||||||
|
const tpl = await res.json();
|
||||||
|
document.getElementById('detailBody').innerHTML = buildDetailBody(tpl);
|
||||||
|
} else {
|
||||||
|
document.getElementById('detailBody').innerHTML = '';
|
||||||
|
}
|
||||||
|
} catch { document.getElementById('detailBody').innerHTML = ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
document.getElementById('detailModal').classList.remove('open');
|
||||||
|
_detailOfferId = null;
|
||||||
|
_detailOffer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailJoin() {
|
||||||
|
const id = _detailOfferId;
|
||||||
|
if (!id) return;
|
||||||
|
closeDetail();
|
||||||
|
openJoinModal(id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detail-Body ────────────────────────────────────────────────────────────
|
||||||
|
function fmtMinutes(min) {
|
||||||
|
if (!min) return '0 Min.';
|
||||||
|
const d = Math.floor(min / 1440), h = Math.floor((min % 1440) / 60), m = min % 60;
|
||||||
|
return [d ? d + 'd' : '', h ? h + 'h' : '', m ? m + 'min' : ''].filter(Boolean).join(' ') || '0 Min.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSection(title, rows) {
|
||||||
|
const rowsHtml = rows.map(([l, v]) =>
|
||||||
|
`<div class="detail-row"><span class="detail-row-label">${esc(l)}</span><span class="detail-row-val">${v}</span></div>`
|
||||||
|
).join('');
|
||||||
|
return `<div class="detail-section"><div class="detail-section-title">${title}</div>${rowsHtml}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDetailBody(t) {
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
if (t.lockType === 'TIMELOCK') {
|
||||||
|
sections.push(buildSection('⏱ Zeit-Einstellungen', [
|
||||||
|
['Mindestdauer', fmtMinutes(t.minTimeInMinutes)],
|
||||||
|
['Maximaldauer', fmtMinutes(t.maxTimeInMinutes)],
|
||||||
|
['Endzeit sichtbar', t.endTimeVisible ? 'Ja' : 'Nein'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
if (t.spinningWheelEntries && t.spinningWheelEntries.length) {
|
||||||
|
const WHEEL_LABELS = {
|
||||||
|
ADD_TIME: '+ Zeit', REMOVE_TIME: '− Zeit', FREEZE_TIME: '❄ Einfrieren für',
|
||||||
|
FREEZE: '🧊 Einfrieren (∞)', UNFREEZE: '🌊 Auftauen', TASK: '🎯 Aufgabe', TEXT: '💬 Text',
|
||||||
|
};
|
||||||
|
const entries = t.spinningWheelEntries.map(e => {
|
||||||
|
const label = WHEEL_LABELS[e.type] || e.type;
|
||||||
|
const extra = e.intVal ? ' ' + fmtMinutes(e.intVal) : (e.stringVal ? ' «' + e.stringVal + '»' : '');
|
||||||
|
return `<span class="detail-wheel-entry">${label}${extra}</span>`;
|
||||||
|
}).join('');
|
||||||
|
sections.push(`<div class="detail-section">
|
||||||
|
<div class="detail-section-title">🎡 Glücksrad (${t.spinningWheelEntries.length} Einträge${t.spinsEveryMinutes ? ', alle ' + fmtMinutes(t.spinsEveryMinutes) : ''})</div>
|
||||||
|
<div>${entries}</div>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.penaltyType) {
|
||||||
|
const penaltyLabels = { ADD: 'Zeit hinzufügen', FREEZE: 'Einfrieren', PILLORY: 'Pranger' };
|
||||||
|
sections.push(buildSection('⚠ Strafmaß', [
|
||||||
|
['Typ', penaltyLabels[t.penaltyType] || t.penaltyType],
|
||||||
|
['Wert', t.penaltyValue ? fmtMinutes(t.penaltyValue) : '–'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.taskEveryMinutes || t.minTasksPerDay) {
|
||||||
|
sections.push(buildSection('🎯 Aufgaben-Timing', [
|
||||||
|
['Intervall', t.taskEveryMinutes ? fmtMinutes(t.taskEveryMinutes) : '–'],
|
||||||
|
['Min./Tag', t.minTasksPerDay ? t.minTasksPerDay + ' Aufgabe(n)' : '–'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.lockType === 'CARDLOCK') {
|
||||||
|
const allKeys = new Set([
|
||||||
|
...Object.keys(t.cardCountsMin || {}),
|
||||||
|
...Object.keys(t.cardCountsMax || {}),
|
||||||
|
]);
|
||||||
|
const rows = [];
|
||||||
|
allKeys.forEach(k => {
|
||||||
|
const mn = (t.cardCountsMin || {})[k] ?? 0;
|
||||||
|
const mx = (t.cardCountsMax || {})[k] ?? 0;
|
||||||
|
if (mn > 0 || mx > 0) rows.push([k, `${mn} – ${mx}`]);
|
||||||
|
});
|
||||||
|
if (rows.length) sections.push(buildSection('🃏 Karten', rows));
|
||||||
|
|
||||||
|
sections.push(buildSection('⚙ Karten-Einstellungen', [
|
||||||
|
['Zieh-Intervall', t.pickEveryMinute ? fmtMinutes(t.pickEveryMinute) : '–'],
|
||||||
|
['Picks kumulieren', t.accumulatePicks ? 'Ja' : 'Nein'],
|
||||||
|
['Verbl. Karten zeigen', t.showRemainingCards ? 'Ja' : 'Nein'],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(buildSection('⚙ Allgemein', [
|
||||||
|
['Hygiene-Öffnung', t.hygieneEnabled ? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen` : 'Keine'],
|
||||||
|
['Verifikation', t.requiresVerification ? 'Erforderlich' : 'Keine'],
|
||||||
|
['Aufgaben-Modus', t.taskMode === 'KEYHOLDER' ? 'Keyholder' : t.taskMode === 'COMMUNITY' ? 'Community' : 'Zufällig'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
if (t.tasks && t.tasks.length) {
|
||||||
|
const taskItems = t.tasks.map(task => {
|
||||||
|
const dur = task.durationMinutes ? ` <span style="color:var(--color-muted);font-size:0.8rem;">(${fmtMinutes(task.durationMinutes)})</span>` : '';
|
||||||
|
const desc = task.description ? `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.1rem;">${esc(task.description)}</div>` : '';
|
||||||
|
return `<div class="detail-task-item">${esc(task.title || task.name || '–')}${dur}${desc}</div>`;
|
||||||
|
}).join('');
|
||||||
|
sections.push(`<div class="detail-section">
|
||||||
|
<div class="detail-section-title">🎯 Aufgaben (${t.tasks.length})</div>${taskItems}
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOffers();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -101,16 +101,154 @@
|
|||||||
.tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
.tp-seg .tp-label { font-size:0.65rem; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.04em; }
|
||||||
.tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; }
|
.tp-colon { font-size:1.1rem; font-weight:700; color:var(--color-muted); margin-bottom:1rem; }
|
||||||
|
|
||||||
|
/* ── Tab-Navigation ── */
|
||||||
|
.kh-tabs { display:flex; gap:0; border-bottom:2px solid var(--color-secondary); margin-bottom:1.25rem; }
|
||||||
|
.kh-tab {
|
||||||
|
padding:0.55rem 1.1rem; font-size:0.9rem; font-weight:600; cursor:pointer;
|
||||||
|
border:none; background:none; color:var(--color-muted);
|
||||||
|
border-bottom:2px solid transparent; margin-bottom:-2px;
|
||||||
|
transition:color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.kh-tab.active { color:var(--color-text); border-bottom-color:var(--color-primary); }
|
||||||
|
|
||||||
|
/* ── Vorlagen-Typ-Icon (wie in meine-locks.html) ── */
|
||||||
|
.template-type-icon {
|
||||||
|
position:relative; width:2.2rem; height:2.2rem; flex-shrink:0;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.template-type-icon .icon-base { font-size:1.8rem; line-height:1; }
|
||||||
|
.template-type-icon img.icon-base { width:1.8rem; height:1.8rem; object-fit:contain; }
|
||||||
|
.template-type-icon .icon-lock {
|
||||||
|
position:absolute; bottom:-2px; right:-4px;
|
||||||
|
font-size:1.5rem; line-height:1;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Angebote-Tab ── */
|
||||||
|
.offer-card {
|
||||||
|
background:var(--color-card); border:1px solid var(--color-secondary);
|
||||||
|
border-radius:10px; padding:0.85rem 1rem; margin-bottom:0.6rem;
|
||||||
|
display:flex; align-items:center; gap:0.85rem;
|
||||||
|
}
|
||||||
|
.offer-card-body { flex:1; min-width:0; }
|
||||||
|
.offer-card-name { font-weight:700; font-size:0.95rem; margin-bottom:0.2rem;
|
||||||
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||||
|
.offer-card-meta { font-size:0.78rem; color:var(--color-muted); display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||||
|
.offer-badge {
|
||||||
|
display:inline-block; font-size:0.72rem; padding:0.1rem 0.45rem;
|
||||||
|
border-radius:4px; background:rgba(255,255,255,0.07); border:1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.offer-badge.direct { background:rgba(46,204,113,0.12); border-color:rgba(46,204,113,0.3); color:#2ecc71; }
|
||||||
|
.offer-badge.confirm { background:rgba(230,126,34,0.12); border-color:rgba(230,126,34,0.3); color:#e67e22; }
|
||||||
|
.btn-offer-del {
|
||||||
|
background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); color:#e74c3c;
|
||||||
|
border-radius:7px; padding:0.3rem 0.65rem; font-size:0.8rem; cursor:pointer; flex-shrink:0; width:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Combobox (Vorlage-Auswahl im Angebot-Modal) */
|
||||||
|
.combo-wrap { position: relative; }
|
||||||
|
.combo-wrap input[type="text"] { width: 100%; box-sizing: border-box; }
|
||||||
|
.combo-dropdown {
|
||||||
|
display: none; position: absolute; top: calc(100% + 3px);
|
||||||
|
left: 0; right: 0; background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary); border-radius: 8px;
|
||||||
|
max-height: 220px; overflow-y: auto; z-index: 600;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.combo-dropdown.open { display: block; }
|
||||||
|
.combo-option {
|
||||||
|
padding: 0.55rem 0.85rem; cursor: pointer;
|
||||||
|
font-size: 0.9rem; color: var(--color-text);
|
||||||
|
}
|
||||||
|
.combo-option:hover { background: var(--color-secondary); }
|
||||||
|
.combo-empty { padding: 0.55rem 0.85rem; font-size: 0.85rem; color: var(--color-muted); font-style: italic; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1 style="margin-bottom:1.25rem;">Keyholder</h1>
|
<h1 style="margin-bottom:1rem;">Keyholder</h1>
|
||||||
|
|
||||||
<!-- Meine Lockees -->
|
<!-- Tab-Navigation -->
|
||||||
<div class="lock-list" id="locksGrid"></div>
|
<div class="kh-tabs">
|
||||||
<p class="empty-hint" id="locksEmpty" style="display:none;">Du bist aktuell bei keinem Lock als Keyholder eingetragen.</p>
|
<button class="kh-tab active" onclick="switchTab('lockees')">Meine Lockees</button>
|
||||||
|
<button class="kh-tab" onclick="switchTab('offers')">Keyholder-Angebote</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Meine Lockees -->
|
||||||
|
<div id="tabLockees">
|
||||||
|
<div class="lock-list" id="locksGrid"></div>
|
||||||
|
<p class="empty-hint" id="locksEmpty" style="display:none;">Du bist aktuell bei keinem Lock als Keyholder eingetragen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Keyholder-Angebote -->
|
||||||
|
<div id="tabOffers" style="display:none;">
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-bottom:1rem;">
|
||||||
|
<button style="width:auto;padding:0.5rem 1.1rem;" onclick="openCreateOfferModal()">+ Angebot erstellen</button>
|
||||||
|
</div>
|
||||||
|
<div id="offersList"></div>
|
||||||
|
<p class="empty-hint" id="offersEmpty" style="display:none;">Du hast noch keine Keyholder-Angebote erstellt.</p>
|
||||||
|
<p id="offersLimitHint" style="display:none;font-size:0.82rem;color:var(--color-muted);margin-top:0.5rem;"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Angebot-Erstellen-Modal -->
|
||||||
|
<div id="createOfferModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
|
||||||
|
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:440px;width:92%;max-height:88vh;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;position:relative;">
|
||||||
|
<button onclick="closeCreateOfferModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;">✕</button>
|
||||||
|
<h3 id="offerModalTitle" style="margin:0;font-size:1.05rem;">🔑 Keyholder-Angebot erstellen</h3>
|
||||||
|
|
||||||
|
<!-- Template-Auswahl -->
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.4rem;">Vorlage</div>
|
||||||
|
<div class="combo-wrap" id="offerTemplateCombo">
|
||||||
|
<input type="text" id="offerTemplateInput" placeholder="Vorlage suchen…" autocomplete="off"
|
||||||
|
style="padding:0.5rem 0.75rem;border-radius:7px;border:1px solid var(--color-secondary);background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;">
|
||||||
|
<div class="combo-dropdown" id="offerTemplateDropdown"></div>
|
||||||
|
<input type="hidden" id="offerTemplateValue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ziel-Geschlechter -->
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.5rem;">Richtet sich an</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.35rem;">
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" id="offerGenderAll" onchange="toggleAllGenders(this)" style="width:auto;"> Alle Geschlechter
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" name="offerGender" value="WEIBLICH" style="width:auto;"> Weiblich
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" name="offerGender" value="MAENNLICH" style="width:auto;"> Männlich
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" name="offerGender" value="DIVERS" style="width:auto;"> Divers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Startmodus -->
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.72rem;font-weight:700;color:var(--color-primary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.5rem;">Startmodus</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.35rem;">
|
||||||
|
<label style="display:flex;align-items:flex-start;gap:0.5rem;font-size:0.88rem;cursor:pointer;line-height:1.4;">
|
||||||
|
<input type="radio" name="offerStartMode" value="direct" checked style="width:auto;margin-top:2px;">
|
||||||
|
<span><strong>Direktstart</strong> – Lock startet sofort, du wirst als Keyholder eingetragen</span>
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:flex-start;gap:0.5rem;font-size:0.88rem;cursor:pointer;line-height:1.4;">
|
||||||
|
<input type="radio" name="offerStartMode" value="confirm" style="width:auto;margin-top:2px;">
|
||||||
|
<span><strong>Mit Bestätigung</strong> – du erhältst eine Einladung und kannst annehmen oder ablehnen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="createOfferError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
|
||||||
|
<div style="display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.25rem;">
|
||||||
|
<button onclick="closeCreateOfferModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;width:auto;">Abbrechen</button>
|
||||||
|
<button id="createOfferSubmitBtn" onclick="submitCreateOffer()" style="padding:0.5rem 1.25rem;border-radius:7px;font-size:0.88rem;font-weight:600;width:auto;" id="createOfferSubmitBtn">Erstellen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -409,6 +547,17 @@
|
|||||||
body.dataset.loaded = '1';
|
body.dataset.loaded = '1';
|
||||||
attachDetailListeners(body, lockId);
|
attachDetailListeners(body, lockId);
|
||||||
}
|
}
|
||||||
|
// Listenkarte line3 aktualisieren
|
||||||
|
const line3 = card ? card.querySelector('.lock-card-line3') : null;
|
||||||
|
if (line3) {
|
||||||
|
const startDate = d.startTime ? new Date(d.startTime).toLocaleDateString('de-DE') : '–';
|
||||||
|
const frozenBadge = (d.isFrozenByKeyholder || d.isFrozen) ? ' · ❄️ Eingefroren' : '';
|
||||||
|
if (d.lockType === 'TIMELOCK') {
|
||||||
|
line3.textContent = `⏱ TimeLock · seit ${startDate}${frozenBadge}`;
|
||||||
|
} else {
|
||||||
|
line3.textContent = `🃏 ${d.totalCards} Karten · seit ${startDate}${frozenBadge}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch(e) { if (body) body.textContent = 'Fehler beim Laden.'; }
|
} catch(e) { if (body) body.textContent = 'Fehler beim Laden.'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +612,24 @@
|
|||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Einfrieren
|
||||||
|
html += `<div class="detail-section"><div class="detail-section-title">Einfrieren</div>`;
|
||||||
|
if (d.isFrozen) {
|
||||||
|
const fu = d.frozenUntil ? new Date(d.frozenUntil).toLocaleString('de-DE') : 'unbegrenzt';
|
||||||
|
html += `<div class="detail-row">
|
||||||
|
<span class="detail-label">❄️ Eingefroren bis</span>
|
||||||
|
<span class="detail-value danger">${fu}</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:0.5rem;">
|
||||||
|
<button onclick="openUnfreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Entfrieren</button>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div style="margin-top:0.25rem;">
|
||||||
|
<button onclick="openFreezeModal('${d.lockId}')" style="background:rgba(52,152,219,0.15);border:1px solid rgba(52,152,219,0.4);color:#3498db;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">❄️ Einfrieren</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
// Gestartet am
|
// Gestartet am
|
||||||
if (d.startTime) {
|
if (d.startTime) {
|
||||||
html += `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.5rem;">
|
html += `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.5rem;">
|
||||||
@@ -804,18 +971,7 @@
|
|||||||
});
|
});
|
||||||
if (res.ok || res.status === 202) {
|
if (res.ok || res.status === 202) {
|
||||||
closeVerificationModal();
|
closeVerificationModal();
|
||||||
const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`);
|
await reloadLockDetail(lockId);
|
||||||
if (cardEl) {
|
|
||||||
const body = cardEl.querySelector('.lock-detail-body');
|
|
||||||
const detailRes = await fetch('/keyholder/as-keyholder/' + lockId);
|
|
||||||
if (detailRes.ok) {
|
|
||||||
const updated = await detailRes.json();
|
|
||||||
lockDetailCache[lockId] = updated;
|
|
||||||
body.innerHTML = buildDetailHtml(updated);
|
|
||||||
body.dataset.loaded = '1';
|
|
||||||
attachDetailListeners(body, lockId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
btnUp.disabled = btnDown.disabled = false;
|
btnUp.disabled = btnDown.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -903,19 +1059,7 @@
|
|||||||
});
|
});
|
||||||
if (res.ok || res.status === 204) {
|
if (res.ok || res.status === 204) {
|
||||||
closeCardModal();
|
closeCardModal();
|
||||||
// Detail neu laden
|
await reloadLockDetail(lockId);
|
||||||
const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`);
|
|
||||||
if (cardEl) {
|
|
||||||
const body = cardEl.querySelector('.lock-detail-body');
|
|
||||||
const detailRes = await fetch('/keyholder/as-keyholder/' + lockId);
|
|
||||||
if (detailRes.ok) {
|
|
||||||
const d = await detailRes.json();
|
|
||||||
lockDetailCache[lockId] = d;
|
|
||||||
body.innerHTML = buildDetailHtml(d);
|
|
||||||
body.dataset.loaded = '1';
|
|
||||||
attachDetailListeners(body, lockId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
const errEl = document.getElementById('cardEditError');
|
const errEl = document.getElementById('cardEditError');
|
||||||
@@ -1270,8 +1414,11 @@
|
|||||||
}
|
}
|
||||||
errEl.style.display = 'none';
|
errEl.style.display = 'none';
|
||||||
const frozenUntil = new Date(Date.now() + minutes * 60000).toISOString().slice(0, 19);
|
const frozenUntil = new Date(Date.now() + minutes * 60000).toISOString().slice(0, 19);
|
||||||
|
const freezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK'
|
||||||
|
? `/keyholder/timelock/as-keyholder/${lockId}/freeze`
|
||||||
|
: `/keyholder/as-keyholder/${lockId}/freeze`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/keyholder/as-keyholder/${lockId}/freeze`, {
|
const res = await fetch(freezeEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ frozenUntil })
|
body: JSON.stringify({ frozenUntil })
|
||||||
@@ -1302,8 +1449,11 @@
|
|||||||
|
|
||||||
async function submitUnfreeze() {
|
async function submitUnfreeze() {
|
||||||
const lockId = unfreezeTargetLockId;
|
const lockId = unfreezeTargetLockId;
|
||||||
|
const unfreezeEndpoint = lockTypeMap[lockId] === 'TIMELOCK'
|
||||||
|
? `/keyholder/timelock/as-keyholder/${lockId}/freeze`
|
||||||
|
: `/keyholder/as-keyholder/${lockId}/freeze`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/keyholder/as-keyholder/${lockId}/freeze`, { method: 'DELETE' });
|
const res = await fetch(unfreezeEndpoint, { method: 'DELETE' });
|
||||||
if (res.ok || res.status === 204) {
|
if (res.ok || res.status === 204) {
|
||||||
closeUnfreezeModal();
|
closeUnfreezeModal();
|
||||||
await reloadLockDetail(lockId);
|
await reloadLockDetail(lockId);
|
||||||
@@ -1311,26 +1461,6 @@
|
|||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadLockDetail(lockId) {
|
|
||||||
const cardEl = document.querySelector(`[data-lock-id="${lockId}"]`);
|
|
||||||
if (!cardEl) return;
|
|
||||||
const body = cardEl.querySelector('.lock-detail-body');
|
|
||||||
const detailRes = await fetch('/keyholder/as-keyholder/' + lockId);
|
|
||||||
if (detailRes.ok) {
|
|
||||||
const updated = await detailRes.json();
|
|
||||||
lockDetailCache[lockId] = updated;
|
|
||||||
body.innerHTML = buildDetailHtml(updated);
|
|
||||||
body.dataset.loaded = '1';
|
|
||||||
attachDetailListeners(body, lockId);
|
|
||||||
// Listenkarte line3 aktualisieren
|
|
||||||
const line3 = cardEl.querySelector('.lock-card-line3');
|
|
||||||
if (line3) {
|
|
||||||
const startDate = updated.startTime ? new Date(updated.startTime).toLocaleDateString('de-DE') : '–';
|
|
||||||
const frozenBadge = updated.isFrozenByKeyholder ? ' · ❄️ Eingefroren' : '';
|
|
||||||
line3.textContent = `🃏 ${updated.totalCards} Karten · seit ${startDate}${frozenBadge}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseTaskForLock(choiceId, taskIndex, lockId) {
|
async function chooseTaskForLock(choiceId, taskIndex, lockId) {
|
||||||
try {
|
try {
|
||||||
@@ -1454,8 +1584,9 @@
|
|||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen
|
// Initial laden, dann ggf. per URL-Parameter ein Lock vorauswählen / Tab wiederherstellen
|
||||||
loadLocks().then(() => {
|
loadLocks().then(() => {
|
||||||
|
restoreTabFromUrl();
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const preselect = params.get('lockId');
|
const preselect = params.get('lockId');
|
||||||
if (preselect) {
|
if (preselect) {
|
||||||
@@ -1474,6 +1605,247 @@
|
|||||||
if (lockId) reloadLockDetail(lockId);
|
if (lockId) reloadLockDetail(lockId);
|
||||||
});
|
});
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
|
// ── Tab-Switching ──────────────────────────────────────────────────────────
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.querySelectorAll('.kh-tab').forEach(b => b.classList.remove('active'));
|
||||||
|
event.currentTarget.classList.add('active');
|
||||||
|
document.getElementById('tabLockees').style.display = tab === 'lockees' ? '' : 'none';
|
||||||
|
document.getElementById('tabOffers').style.display = tab === 'offers' ? '' : 'none';
|
||||||
|
if (tab === 'offers') loadOffers();
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (tab === 'lockees') url.searchParams.delete('tab');
|
||||||
|
else url.searchParams.set('tab', tab);
|
||||||
|
history.replaceState(null, '', url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreTabFromUrl() {
|
||||||
|
const tab = new URLSearchParams(window.location.search).get('tab');
|
||||||
|
if (tab === 'offers') {
|
||||||
|
const btn = document.querySelector('.kh-tab:nth-child(2)');
|
||||||
|
if (btn) { btn.click(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Keyholder-Angebote ────────────────────────────────────────────────────
|
||||||
|
let _offerTemplates = [];
|
||||||
|
let _editOfferId = null;
|
||||||
|
|
||||||
|
function esc2(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||||
|
|
||||||
|
const GENDER_LABELS = { WEIBLICH: 'Weiblich', MAENNLICH: 'Männlich', DIVERS: 'Divers' };
|
||||||
|
|
||||||
|
async function loadOffers() {
|
||||||
|
const res = await fetch('/keyholder-offers/mine');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const offers = await res.json();
|
||||||
|
const list = document.getElementById('offersList');
|
||||||
|
const empty = document.getElementById('offersEmpty');
|
||||||
|
list.innerHTML = '';
|
||||||
|
if (offers.length === 0) { empty.style.display = ''; return; }
|
||||||
|
empty.style.display = 'none';
|
||||||
|
offers.forEach(o => list.appendChild(buildOfferCard(o)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOfferCard(o) {
|
||||||
|
const genderTags = (o.targetGenders && o.targetGenders.length > 0)
|
||||||
|
? o.targetGenders.map(g => `<span class="offer-badge">${esc2(GENDER_LABELS[g] || g)}</span>`).join('')
|
||||||
|
: '<span class="offer-badge">Alle</span>';
|
||||||
|
const modeBadge = o.directStart
|
||||||
|
? '<span class="offer-badge direct">Direktstart</span>'
|
||||||
|
: '<span class="offer-badge confirm">Mit Bestätigung</span>';
|
||||||
|
const typeIcon = o.templateType === 'TIMELOCK'
|
||||||
|
? `<div class="template-type-icon"><span class="icon-base">🕐</span><span class="icon-lock">🔒</span></div>`
|
||||||
|
: `<div class="template-type-icon"><img src="img/card.png" class="icon-base" alt="Karten-Lock"><span class="icon-lock">🔒</span></div>`;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'offer-card';
|
||||||
|
div.style.cursor = 'pointer';
|
||||||
|
div.innerHTML = `
|
||||||
|
${typeIcon}
|
||||||
|
<div class="offer-card-body" style="flex:1;min-width:0;">
|
||||||
|
<div class="offer-card-name">${esc2(o.templateName)}</div>
|
||||||
|
<div class="offer-card-meta">
|
||||||
|
${modeBadge} ${genderTags}
|
||||||
|
<span class="offer-badge">✓ ${o.acceptanceCount}× angenommen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-offer-del" onclick="event.stopPropagation();deleteOffer('${o.id}')">Entfernen</button>`;
|
||||||
|
div.addEventListener('click', () => openEditOfferModal(o));
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOffer(id) {
|
||||||
|
if (!confirm('Angebot wirklich entfernen?')) return;
|
||||||
|
const res = await fetch(`/keyholder-offers/${id}`, { method: 'DELETE' });
|
||||||
|
if (res.ok || res.status === 204) loadOffers();
|
||||||
|
else alert('Fehler beim Entfernen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Angebot erstellen / bearbeiten ────────────────────────────────────────
|
||||||
|
async function _openOfferModal() {
|
||||||
|
document.getElementById('createOfferError').style.display = 'none';
|
||||||
|
document.getElementById('createOfferSubmitBtn').disabled = false;
|
||||||
|
|
||||||
|
// Templates laden (eigene + abonnierte)
|
||||||
|
const [ownRes, subRes] = await Promise.all([
|
||||||
|
fetch('/templates/mine'),
|
||||||
|
fetch('/templates/subscribed')
|
||||||
|
]);
|
||||||
|
const own = ownRes.ok ? await ownRes.json() : [];
|
||||||
|
const sub = subRes.ok ? await subRes.json() : [];
|
||||||
|
const ownIds = new Set(own.map(t => t.templateId));
|
||||||
|
_offerTemplates = [...own, ...sub.filter(t => !ownIds.has(t.templateId))];
|
||||||
|
setupOfferTemplateCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCreateOfferModal() {
|
||||||
|
_editOfferId = null;
|
||||||
|
document.getElementById('offerModalTitle').textContent = '🔑 Keyholder-Angebot erstellen';
|
||||||
|
document.getElementById('createOfferSubmitBtn').textContent = 'Erstellen';
|
||||||
|
|
||||||
|
await _openOfferModal();
|
||||||
|
|
||||||
|
document.getElementById('offerTemplateInput').value = '';
|
||||||
|
document.getElementById('offerTemplateValue').value = '';
|
||||||
|
document.getElementById('offerGenderAll').checked = false;
|
||||||
|
document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = false; });
|
||||||
|
document.querySelector('input[name="offerStartMode"][value="direct"]').checked = true;
|
||||||
|
document.getElementById('createOfferModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openEditOfferModal(o) {
|
||||||
|
_editOfferId = o.id;
|
||||||
|
document.getElementById('offerModalTitle').textContent = '✏️ Keyholder-Angebot bearbeiten';
|
||||||
|
document.getElementById('createOfferSubmitBtn').textContent = 'Speichern';
|
||||||
|
|
||||||
|
await _openOfferModal();
|
||||||
|
|
||||||
|
// Vorlage vorauswählen
|
||||||
|
const tpl = _offerTemplates.find(t => t.templateId === o.templateId);
|
||||||
|
if (tpl) {
|
||||||
|
const badge = tpl.lockType === 'TIMELOCK' ? '⏱' : '🃏';
|
||||||
|
document.getElementById('offerTemplateInput').value = badge + ' ' + (tpl.name || 'Unbenannte Vorlage');
|
||||||
|
document.getElementById('offerTemplateValue').value = tpl.templateId;
|
||||||
|
} else {
|
||||||
|
// Template nicht mehr in eigenen/abonnierten → Fallback: Name anzeigen
|
||||||
|
document.getElementById('offerTemplateInput').value = o.templateName || '';
|
||||||
|
document.getElementById('offerTemplateValue').value = o.templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geschlechter vorauswählen
|
||||||
|
const genders = o.targetGenders || [];
|
||||||
|
const allSelected = genders.length === 0;
|
||||||
|
document.getElementById('offerGenderAll').checked = allSelected;
|
||||||
|
document.querySelectorAll('input[name="offerGender"]').forEach(cb => {
|
||||||
|
cb.checked = !allSelected && genders.includes(cb.value);
|
||||||
|
cb.disabled = allSelected;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startmodus
|
||||||
|
const modeVal = o.directStart ? 'direct' : 'confirm';
|
||||||
|
document.querySelector(`input[name="offerStartMode"][value="${modeVal}"]`).checked = true;
|
||||||
|
|
||||||
|
document.getElementById('createOfferModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupOfferTemplateCombo() {
|
||||||
|
const input = document.getElementById('offerTemplateInput');
|
||||||
|
const dropdown = document.getElementById('offerTemplateDropdown');
|
||||||
|
const hidden = document.getElementById('offerTemplateValue');
|
||||||
|
|
||||||
|
// Vorherige Listener entfernen durch Klonen
|
||||||
|
const newInput = input.cloneNode(true);
|
||||||
|
input.parentNode.replaceChild(newInput, input);
|
||||||
|
|
||||||
|
function renderDropdown(query) {
|
||||||
|
const q = query.toLowerCase().trim();
|
||||||
|
const filtered = q
|
||||||
|
? _offerTemplates.filter(t => (t.name || '').toLowerCase().includes(q))
|
||||||
|
: _offerTemplates;
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
dropdown.innerHTML = '<div class="combo-empty">Keine Vorlagen gefunden.</div>';
|
||||||
|
} else {
|
||||||
|
filtered.forEach(t => {
|
||||||
|
const badge = t.lockType === 'TIMELOCK' ? '⏱' : '🃏';
|
||||||
|
const label = t.name || 'Unbenannte Vorlage';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'combo-option';
|
||||||
|
div.innerHTML = `${badge} ${label}`;
|
||||||
|
div.addEventListener('mousedown', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
hidden.value = t.templateId;
|
||||||
|
newInput.value = badge + ' ' + label;
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
});
|
||||||
|
dropdown.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dropdown.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
newInput.addEventListener('input', () => { hidden.value = ''; renderDropdown(newInput.value); });
|
||||||
|
newInput.addEventListener('focus', () => renderDropdown(newInput.value));
|
||||||
|
newInput.addEventListener('blur', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
if (!hidden.value) newInput.value = '';
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateOfferModal() {
|
||||||
|
document.getElementById('createOfferModal').style.display = 'none';
|
||||||
|
_editOfferId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllGenders(allCb) {
|
||||||
|
document.querySelectorAll('input[name="offerGender"]').forEach(cb => { cb.checked = false; cb.disabled = allCb.checked; });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCreateOffer() {
|
||||||
|
const templateId = document.getElementById('offerTemplateValue').value;
|
||||||
|
if (!templateId) { showOfferError('Bitte eine Vorlage auswählen.'); return; }
|
||||||
|
|
||||||
|
const allGenders = document.getElementById('offerGenderAll').checked;
|
||||||
|
const targetGenders = allGenders
|
||||||
|
? []
|
||||||
|
: Array.from(document.querySelectorAll('input[name="offerGender"]:checked')).map(cb => cb.value);
|
||||||
|
|
||||||
|
const directStart = document.querySelector('input[name="offerStartMode"]:checked').value === 'direct';
|
||||||
|
|
||||||
|
const btn = document.getElementById('createOfferSubmitBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
const url = _editOfferId ? `/keyholder-offers/${_editOfferId}` : '/keyholder-offers';
|
||||||
|
const method = _editOfferId ? 'PATCH' : 'POST';
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ templateId, targetGenders, directStart })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
closeCreateOfferModal();
|
||||||
|
loadOffers();
|
||||||
|
} else if (res.status === 403) {
|
||||||
|
const d = await res.json().catch(() => ({}));
|
||||||
|
showOfferError(d.error === 'offer_limit_reached'
|
||||||
|
? 'Limit erreicht – upgrade auf Premium für unbegrenzte Angebote.'
|
||||||
|
: 'Keine Berechtigung.');
|
||||||
|
btn.disabled = false;
|
||||||
|
} else {
|
||||||
|
showOfferError(_editOfferId ? 'Fehler beim Speichern.' : 'Fehler beim Erstellen.');
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOfferError(msg) {
|
||||||
|
const el = document.getElementById('createOfferError');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.display = '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Aufgaben-Auswahl-Popup -->
|
<!-- Aufgaben-Auswahl-Popup -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Meine Locks – xXx Sphere</title>
|
<title>Meine Vorlagen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
display:none; position:fixed; inset:0;
|
display:none; position:fixed; inset:0;
|
||||||
background:rgba(0,0,0,0.65); z-index:400;
|
background:rgba(0,0,0,0.65); z-index:400;
|
||||||
align-items:flex-start; overflow-y:auto; padding:2rem 0;
|
align-items:flex-start; justify-content:center; overflow-y:auto; padding:2rem 1rem;
|
||||||
}
|
}
|
||||||
.modal-backdrop.open { display:flex; }
|
.modal-backdrop.open { display:flex; }
|
||||||
.modal-box {
|
.modal-box {
|
||||||
@@ -234,13 +234,36 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;gap:1rem;flex-wrap:wrap;">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;gap:1rem;flex-wrap:wrap;">
|
||||||
<h1 style="margin:0;">Meine Locks</h1>
|
<h1 style="margin:0;">Meine Vorlagen</h1>
|
||||||
<button onclick="openModal()" style="width:auto;padding:0.55rem 1.2rem;">+ Vorlage erstellen</button>
|
<button onclick="openModal()" style="width:auto;padding:0.55rem 1.2rem;">+ Vorlage erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="template-list" id="templateList"></div>
|
<div class="template-list" id="templateList"></div>
|
||||||
<p class="empty-hint" id="listEmpty" style="display:none;">Noch keine Vorlagen vorhanden.</p>
|
<p class="empty-hint" id="listEmpty" style="display:none;">Noch keine Vorlagen vorhanden.</p>
|
||||||
<div id="scrollSentinel" style="height:1px;"></div>
|
<div id="scrollSentinel" style="height:1px;"></div>
|
||||||
|
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
|
||||||
|
<div class="template-list" id="subscribedList"></div>
|
||||||
|
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
|
||||||
|
<div id="subscribedSentinel" style="height:1px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Veröffentlichen-Modal -->
|
||||||
|
<div class="modal-backdrop" id="publishModal" onclick="closePublishModal()">
|
||||||
|
<div class="modal-box" style="max-width:380px;max-height:none;" onclick="event.stopPropagation()">
|
||||||
|
<h2 style="margin:0 0 1rem;">Vorlage veröffentlichen</h2>
|
||||||
|
<p style="font-size:0.88rem;color:var(--color-muted);margin:0 0 1rem;line-height:1.5;">
|
||||||
|
Die Vorlage wird öffentlich sichtbar und kann von anderen Nutzern abonniert werden.
|
||||||
|
Wenn du die Veröffentlichung entfernst, werden alle Abonnements gelöscht – angefertigte Kopien bleiben erhalten.
|
||||||
|
</p>
|
||||||
|
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 1.25rem;line-height:1.5;">
|
||||||
|
Ob dein Name angezeigt wird, hängt von deiner Datenschutzeinstellung
|
||||||
|
<em>„Profil bei Veröffentlichungen sichtbar"</em> ab.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
|
||||||
|
<button onclick="closePublishModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);width:auto;padding:0.5rem 1.1rem;">Abbrechen</button>
|
||||||
|
<button id="publishConfirmBtn" onclick="confirmPublish()" style="width:auto;padding:0.5rem 1.25rem;">Veröffentlichen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -804,8 +827,7 @@
|
|||||||
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
const rect = document.querySelector('.content')?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
const box = document.querySelector('.modal-box');
|
const box = document.querySelector('.modal-box');
|
||||||
box.style.width = rect.width + 'px';
|
box.style.width = Math.min(rect.width, 720) + 'px';
|
||||||
box.style.marginLeft = rect.left + 'px';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal(template) {
|
function openModal(template) {
|
||||||
@@ -1036,6 +1058,12 @@
|
|||||||
firstError = firstError || document.getElementById('modalError');
|
firstError = firstError || document.getElementById('modalError');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validierung: Unbegrenztes FREEZE ohne UNFREEZE
|
||||||
|
if (wheelEntries.some(e => e.type === 'FREEZE') && !wheelEntries.some(e => e.type === 'UNFREEZE')) {
|
||||||
|
showModalError('Das Glücksrad enthält ein unbegrenztes Einfrieren (FREEZE), aber keinen Auftau-Eintrag (UNFREEZE). Bitte einen UNFREEZE-Eintrag hinzufügen.');
|
||||||
|
firstError = firstError || document.getElementById('modalError');
|
||||||
|
}
|
||||||
|
|
||||||
// Validierung: Mindestaufgaben pro Tag (Zeitkollision)
|
// Validierung: Mindestaufgaben pro Tag (Zeitkollision)
|
||||||
const taskMode = document.querySelector('input[name="modalTaskMode"]:checked')?.value || 'RANDOM';
|
const taskMode = document.querySelector('input[name="modalTaskMode"]:checked')?.value || 'RANDOM';
|
||||||
if (taskEvery && minTasksPerDay) {
|
if (taskEvery && minTasksPerDay) {
|
||||||
@@ -1118,6 +1146,12 @@
|
|||||||
? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen`
|
? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen`
|
||||||
: 'Keine';
|
: 'Keine';
|
||||||
const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${t.taskCount ? ' · ' + t.taskCount + ' Aufgabe(n)' : ''}`;
|
const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${t.taskCount ? ' · ' + t.taskCount + ' Aufgabe(n)' : ''}`;
|
||||||
|
const publishedBadge = t.published
|
||||||
|
? `<span style="font-size:0.7rem;background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;border-radius:5px;padding:0.15rem 0.5rem;margin-left:0.4rem;">🌐 Veröffentlicht</span>`
|
||||||
|
: '';
|
||||||
|
const publishBtn = t.published
|
||||||
|
? `<button onclick="event.stopPropagation();unpublishTemplate('${t.templateId}','${esc(t.name||'')}')" style="background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.35);color:#2ecc71;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">🌐 Entfernen</button>`
|
||||||
|
: `<button onclick="event.stopPropagation();openPublishModal('${t.templateId}')" style="background:rgba(52,152,219,0.1);border:1px solid rgba(52,152,219,0.35);color:#3498db;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">🌐 Veröffentlichen</button>`;
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'template-card';
|
card.className = 'template-card';
|
||||||
@@ -1129,10 +1163,11 @@
|
|||||||
<span class="icon-lock">🔒</span>
|
<span class="icon-lock">🔒</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1; min-width:0;">
|
<div style="flex:1; min-width:0;">
|
||||||
<div class="template-name">${esc(t.name || 'Ohne Namen')}</div>
|
<div class="template-name">${esc(t.name || 'Ohne Namen')}${publishedBadge}</div>
|
||||||
<div class="template-meta">${metaLine}</div>
|
<div class="template-meta">${metaLine}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="template-actions">
|
<div class="template-actions">
|
||||||
|
${publishBtn}
|
||||||
<button onclick="event.stopPropagation();deleteTemplate('${t.lockType}','${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;">✕ Löschen</button>
|
<button onclick="event.stopPropagation();deleteTemplate('${t.lockType}','${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;">✕ Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1164,6 +1199,7 @@
|
|||||||
document.getElementById('templateList').innerHTML = '';
|
document.getElementById('templateList').innerHTML = '';
|
||||||
document.getElementById('listEmpty').style.display = 'none';
|
document.getElementById('listEmpty').style.display = 'none';
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
|
loadSubscribedTemplates();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editTemplate(id) {
|
async function editTemplate(id) {
|
||||||
@@ -1172,6 +1208,94 @@
|
|||||||
openModal(await res.json());
|
openModal(await res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Abonnierte Vorlagen ──
|
||||||
|
async function loadSubscribedTemplates() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/templates/subscribed');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const list = await res.json();
|
||||||
|
const el = document.getElementById('subscribedList');
|
||||||
|
el.innerHTML = '';
|
||||||
|
if (!list.length) {
|
||||||
|
document.getElementById('subscribedEmpty').style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('subscribedEmpty').style.display = 'none';
|
||||||
|
list.forEach(t => appendSubscribedCard(t));
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSubscribedCard(t) {
|
||||||
|
const list = document.getElementById('subscribedList');
|
||||||
|
const isCard = t.lockType === 'CARDLOCK';
|
||||||
|
const typeIcon = isCard
|
||||||
|
? `<img src="img/card.png" class="icon-base" alt="Karten-Lock">`
|
||||||
|
: `<span class="icon-base">🕐</span>`;
|
||||||
|
const authorText = t.authorName ? ` · von ${esc(t.authorName)}` : '';
|
||||||
|
const subsText = `${t.subscriberCount} Abonnent(en)`;
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'template-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="template-card-header">
|
||||||
|
<div class="template-type-icon">
|
||||||
|
${typeIcon}
|
||||||
|
<span class="icon-lock">🔒</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:0;">
|
||||||
|
<div class="template-name">${esc(t.name || 'Ohne Namen')}</div>
|
||||||
|
<div class="template-meta">${isCard ? '🃏 Karten-Lock' : '⏱ Zeit-Lock'}${authorText} · ${subsText}</div>
|
||||||
|
</div>
|
||||||
|
<div class="template-actions">
|
||||||
|
<button onclick="forkTemplate('${t.templateId}')" style="background:rgba(52,152,219,0.1);border:1px solid rgba(52,152,219,0.35);color:#3498db;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">📋 Kopie</button>
|
||||||
|
<button onclick="cancelSubscription('${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;width:auto;padding:0.35rem 0.75rem;font-size:0.8rem;">✕ Abo</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
list.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forkTemplate(id) {
|
||||||
|
const btn = event.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/templates/${id}/fork`, { method: 'POST' });
|
||||||
|
if (res.ok) { resetList(); }
|
||||||
|
else { alert('Kopie konnte nicht erstellt werden.'); btn.disabled = false; }
|
||||||
|
} catch(e) { btn.disabled = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelSubscription(id, name) {
|
||||||
|
if (!confirm(`Abonnement von „${name}" wirklich kündigen?`)) return;
|
||||||
|
const res = await fetch(`/templates/${id}/subscribe`, { method: 'DELETE' });
|
||||||
|
if (res.ok || res.status === 204) loadSubscribedTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Veröffentlichen ──
|
||||||
|
let _publishTemplateId = null;
|
||||||
|
function openPublishModal(id) {
|
||||||
|
_publishTemplateId = id;
|
||||||
|
document.getElementById('publishModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
function closePublishModal() {
|
||||||
|
document.getElementById('publishModal').style.display = 'none';
|
||||||
|
_publishTemplateId = null;
|
||||||
|
}
|
||||||
|
async function confirmPublish() {
|
||||||
|
if (!_publishTemplateId) return;
|
||||||
|
const btn = document.getElementById('publishConfirmBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/templates/${_publishTemplateId}/publish`, { method: 'PATCH' });
|
||||||
|
if (res.ok || res.status === 204) { closePublishModal(); resetList(); }
|
||||||
|
else { alert('Fehler beim Veröffentlichen.'); btn.disabled = false; }
|
||||||
|
} catch(e) { btn.disabled = false; }
|
||||||
|
}
|
||||||
|
async function unpublishTemplate(id, name) {
|
||||||
|
if (!confirm(`Veröffentlichung von „${name}" entfernen? Alle Abonnements werden gelöscht.`)) return;
|
||||||
|
const res = await fetch(`/templates/${id}/publish`, { method: 'DELETE' });
|
||||||
|
if (res.ok || res.status === 204) resetList();
|
||||||
|
}
|
||||||
|
|
||||||
// ── IntersectionObserver für Infinite Scroll ──
|
// ── IntersectionObserver für Infinite Scroll ──
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
if (entries[0].isIntersecting) loadNextPage();
|
if (entries[0].isIntersecting) loadNextPage();
|
||||||
|
|||||||
@@ -360,17 +360,19 @@
|
|||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
myUserName = user.name;
|
myUserName = user.name;
|
||||||
|
|
||||||
// Subscription + Templates + TTLock-Config parallel laden
|
// Subscription + Templates (eigene + abonnierte) + TTLock-Config parallel laden
|
||||||
try {
|
try {
|
||||||
const [cardTpls, timeTpls, subData, ttlCfg] = await Promise.all([
|
const [ownTpls, subTpls, subData, ttlCfg] = await Promise.all([
|
||||||
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
|
fetch('/templates/mine').then(r => r.ok ? r.json() : []),
|
||||||
fetch('/timelock/templates').then(r => r.ok ? r.json() : []),
|
fetch('/templates/subscribed').then(r => r.ok ? r.json() : []),
|
||||||
fetch('/subscription/me').then(r => r.ok ? r.json() : null),
|
fetch('/subscription/me').then(r => r.ok ? r.json() : null),
|
||||||
fetch('/user/me/ttlock').then(r => r.ok ? r.json() : null)
|
fetch('/user/me/ttlock').then(r => r.ok ? r.json() : null)
|
||||||
]);
|
]);
|
||||||
|
const toEntry = t => ({ ...t, _type: t.lockType === 'TIMELOCK' ? 'timelock' : 'cardlock' });
|
||||||
|
const ownIds = new Set(ownTpls.map(t => t.templateId));
|
||||||
allTemplates = [
|
allTemplates = [
|
||||||
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
|
...ownTpls.map(toEntry),
|
||||||
...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
|
...subTpls.filter(t => !ownIds.has(t.templateId)).map(toEntry)
|
||||||
];
|
];
|
||||||
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
|
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
|
||||||
ttlockReady = !!(ttlCfg && ttlCfg.testSuccessful);
|
ttlockReady = !!(ttlCfg && ttlCfg.testSuccessful);
|
||||||
@@ -717,6 +719,11 @@
|
|||||||
errors.push('Aufgaben sind zeitlich konfiguriert, aber keine Aufgaben in der Vorlage definiert. Bitte die Vorlage bearbeiten.');
|
errors.push('Aufgaben sind zeitlich konfiguriert, aber keine Aufgaben in der Vorlage definiert. Bitte die Vorlage bearbeiten.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unbegrenztes Einfrieren ohne Auftau-Eintrag
|
||||||
|
if (spinEntries.some(e => e.type === 'FREEZE') && !spinEntries.some(e => e.type === 'UNFREEZE')) {
|
||||||
|
errors.push('Das Spinning Wheel enthält ein unbegrenztes Einfrieren (FREEZE), aber keinen Auftau-Eintrag (UNFREEZE). Das Lock könnte dauerhaft eingefroren bleiben.');
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user