Compare commits
No commits in common. "main" and "develop" have entirely different histories.
|
|
@ -6,6 +6,7 @@ src/.env.staging
|
|||
src/.env.production
|
||||
src/vendor/
|
||||
src/node_modules/
|
||||
src/public/build/
|
||||
src/storage/logs/
|
||||
src/storage/framework/cache/
|
||||
src/storage/framework/sessions/
|
||||
|
|
|
|||
91
deploy.sh
91
deploy.sh
|
|
@ -1,87 +1,22 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
MODE=${1:-production}
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "🚀 Aziros deploying..."
|
||||
|
||||
if [ "$MODE" = "staging" ]; then
|
||||
COMPOSE="docker-compose.staging.yml"
|
||||
BUILD_CMD="npm run build:staging"
|
||||
else
|
||||
COMPOSE="docker-compose.yml"
|
||||
BUILD_CMD="npm run build:prod"
|
||||
fi
|
||||
cd ~/aziros
|
||||
|
||||
echo "🚀 Aziros deploying... ($MODE)"
|
||||
|
||||
# Variablen für Docker Compose exportieren
|
||||
set -a
|
||||
source "$SCRIPT_DIR/src/.env"
|
||||
set +a
|
||||
|
||||
# Git Pull
|
||||
echo "→ Code aktualisieren..."
|
||||
git pull origin main
|
||||
|
||||
# NPM Build
|
||||
echo "→ Assets bauen..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
npm ci --silent
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
$BUILD_CMD
|
||||
|
||||
# Composer
|
||||
echo "→ Composer install..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
composer install --no-dev \
|
||||
--optimize-autoloader
|
||||
|
||||
# Verzeichnisse sicherstellen
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
mkdir -p \
|
||||
bootstrap/cache \
|
||||
storage/framework/cache \
|
||||
storage/framework/sessions \
|
||||
storage/framework/views \
|
||||
storage/logs
|
||||
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
chmod -R 775 bootstrap/cache storage
|
||||
|
||||
# Migrations
|
||||
echo "→ Migrationen..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan migrate --force
|
||||
|
||||
# Translations
|
||||
echo "→ Translations synchronisieren..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan db:seed --class=TranslationSeeder --force
|
||||
# Migrations ausführen
|
||||
docker compose exec app php artisan migrate --force
|
||||
|
||||
# Cache leeren
|
||||
echo "→ Cache leeren..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan config:clear
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan cache:clear
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan view:clear
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan route:clear
|
||||
docker compose exec app php artisan config:clear
|
||||
docker compose exec app php artisan cache:clear
|
||||
docker compose exec app php artisan view:clear
|
||||
docker compose exec app php artisan route:clear
|
||||
|
||||
# Cache neu aufbauen
|
||||
echo "→ Cache aufbauen..."
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan config:cache
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan route:cache
|
||||
docker compose -f $COMPOSE exec -T app \
|
||||
php artisan view:cache
|
||||
# Caches neu aufbauen
|
||||
docker compose exec app php artisan config:cache
|
||||
docker compose exec app php artisan route:cache
|
||||
docker compose exec app php artisan view:cache
|
||||
|
||||
# Services neu starten
|
||||
echo "→ Services neu starten..."
|
||||
docker compose -f $COMPOSE restart \
|
||||
worker scheduler mail-worker reverb
|
||||
|
||||
echo "✅ Deploy fertig! ($MODE)"
|
||||
echo "✅ Deploy fertig!"
|
||||
|
|
@ -16,7 +16,7 @@ services:
|
|||
volumes:
|
||||
- ./db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$DB_ROOT_PASSWORD || exit 1"]
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ services:
|
|||
volumes:
|
||||
- ./db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$DB_ROOT_PASSWORD || exit 1"]
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
|
@ -163,24 +163,6 @@ services:
|
|||
depends_on:
|
||||
- redis
|
||||
|
||||
deployer:
|
||||
image: almir/webhook
|
||||
container_name: nexxo_deployer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9000:9000"
|
||||
env_file: src/.env
|
||||
volumes:
|
||||
- ./docker/webhook/hooks.json:/etc/webhook/hooks.json:ro
|
||||
- ./docker/webhook/entrypoint.sh:/entrypoint.sh:ro
|
||||
- ./docker/webhook/deploy.sh:/scripts/deploy.sh
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /usr/bin/docker:/usr/bin/docker:ro
|
||||
- ./:/aziros
|
||||
entrypoint: ["/bin/sh", "/entrypoint.sh"]
|
||||
networks:
|
||||
- nexxo
|
||||
|
||||
networks:
|
||||
nexxo:
|
||||
driver: bridge
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ services:
|
|||
volumes:
|
||||
- ./db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$DB_ROOT_PASSWORD || exit 1"]
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
|
|
|||
|
|
@ -24,16 +24,6 @@ server {
|
|||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ^~ /build/ {
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods
|
||||
"GET, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers
|
||||
"Origin, Content-Type, Accept" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
|
||||
# PHP Handling
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
COMPOSE_FILE="/aziros/docker-compose.staging.yml"
|
||||
|
||||
echo "[deploy] $(date) – Deploy gestartet"
|
||||
cd /aziros
|
||||
|
||||
git pull origin main
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app npm ci --silent
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app npm run build:staging
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app composer install --no-dev --optimize-autoloader --quiet
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan migrate --force
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan db:seed --class=TranslationSeeder --force
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan config:clear
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan cache:clear
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan view:clear
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan route:clear
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan config:cache
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan route:cache
|
||||
docker compose -f "$COMPOSE_FILE" exec -T app php artisan view:cache
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" restart worker scheduler mail-worker reverb
|
||||
|
||||
echo "[deploy] ✅ Deploy fertig"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
sed "s|\${DEPLOY_WEBHOOK_SECRET}|${DEPLOY_WEBHOOK_SECRET}|g" /etc/webhook/hooks.json > /tmp/hooks.json
|
||||
exec webhook -hooks /tmp/hooks.json -verbose -port 9000 -urlprefix ""
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": "deploy",
|
||||
"execute-command": "/scripts/deploy.sh",
|
||||
"command-working-directory": "/aziros",
|
||||
"response-message": "Deploy gestartet",
|
||||
"trigger-rule": {
|
||||
"match": {
|
||||
"type": "value",
|
||||
"value": "${DEPLOY_WEBHOOK_SECRET}",
|
||||
"parameter": {
|
||||
"source": "header",
|
||||
"name": "X-Webhook-Secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
315
setup.sh
315
setup.sh
|
|
@ -1,315 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
MODE=${1:-production}
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Aziros Server Setup ║"
|
||||
echo "║ Mode: $MODE ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# ROOT CHECK
|
||||
# ═══════════════════════════════════════
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "❌ Bitte als root ausführen"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 1 — System Update
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ System wird aktualisiert..."
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl git nano ufw \
|
||||
ca-certificates gnupg2 \
|
||||
apt-transport-https net-tools \
|
||||
openssh-server
|
||||
echo "✅ System aktualisiert"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 2 — User anlegen
|
||||
# ═══════════════════════════════════════
|
||||
USER="nexxo"
|
||||
if id "$USER" &>/dev/null; then
|
||||
echo "→ User $USER existiert bereits"
|
||||
else
|
||||
echo "→ User '$USER' wird erstellt..."
|
||||
adduser --gecos "" $USER
|
||||
usermod -aG sudo $USER
|
||||
echo "✅ User $USER erstellt"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 3 — Docker installieren
|
||||
# ═══════════════════════════════════════
|
||||
if command -v docker &>/dev/null; then
|
||||
echo "→ Docker bereits installiert"
|
||||
else
|
||||
echo "→ Docker wird installiert..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
echo "✅ Docker installiert"
|
||||
fi
|
||||
usermod -aG docker $USER
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 4 — Firewall
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ Firewall wird konfiguriert..."
|
||||
ufw allow OpenSSH
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw allow 8080/tcp
|
||||
ufw --force enable
|
||||
echo "✅ Firewall konfiguriert"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 5 — Git konfigurieren
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ Git wird konfiguriert..."
|
||||
sudo -u $USER git config \
|
||||
--global credential.helper store
|
||||
sudo -u $USER git config \
|
||||
--global user.name "Aziros Deploy"
|
||||
sudo -u $USER git config \
|
||||
--global user.email "deploy@aziros.com"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 6 — Git Token + Repo clonen
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ Gitea Access Token eingeben:"
|
||||
echo " (Gitea → Einstellungen → Anwendungen → Token erstellen)"
|
||||
read -sp "Token: " GIT_TOKEN
|
||||
echo ""
|
||||
|
||||
# Credentials speichern:
|
||||
echo "https://token:$GIT_TOKEN@git.nexlab.at" > \
|
||||
/home/$USER/.git-credentials
|
||||
chmod 600 /home/$USER/.git-credentials
|
||||
chown $USER:$USER /home/$USER/.git-credentials
|
||||
|
||||
# Git credential helper setzen:
|
||||
sudo -u $USER git config \
|
||||
--global credential.helper store
|
||||
|
||||
# Repo URL mit Token:
|
||||
REPO="https://token:$GIT_TOKEN@git.nexlab.at/boban/aziros.git"
|
||||
|
||||
echo "→ Repository wird geklont..."
|
||||
if [ -d "/home/$USER/aziros/.git" ]; then
|
||||
echo "→ Repo existiert — pull"
|
||||
git config --global --add safe.directory \
|
||||
/home/$USER/aziros
|
||||
sudo -u $USER git -C \
|
||||
/home/$USER/aziros pull origin main
|
||||
else
|
||||
rm -rf /home/$USER/aziros
|
||||
sudo -u $USER git clone \
|
||||
$REPO /home/$USER/aziros
|
||||
git config --global --add safe.directory \
|
||||
/home/$USER/aziros
|
||||
fi
|
||||
echo "✅ Repository bereit"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 7 — .env vom lokalen Server holen
|
||||
# ═══════════════════════════════════════
|
||||
ENV_FILE="/home/$USER/aziros/src/.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo ""
|
||||
echo "→ .env wird vom lokalen Server geholt..."
|
||||
echo ""
|
||||
read -p "Lokale Server IP: " LOCAL_IP
|
||||
read -p "Lokaler User (z.B. nexxo): " LOCAL_USER
|
||||
read -sp "Passwort von $LOCAL_USER@$LOCAL_IP: " \
|
||||
LOCAL_PASS
|
||||
echo ""
|
||||
|
||||
if [ "$MODE" = "staging" ]; then
|
||||
SRC_ENV=".env.staging"
|
||||
elif [ "$MODE" = "development" ]; then
|
||||
SRC_ENV=".env.development"
|
||||
else
|
||||
SRC_ENV=".env.production"
|
||||
fi
|
||||
|
||||
# sshpass installieren:
|
||||
apt install -y sshpass -q
|
||||
|
||||
# SSH Key erstellen falls nicht vorhanden:
|
||||
if [ ! -f "/home/$USER/.ssh/id_ed25519" ]; then
|
||||
echo "→ SSH Key wird erstellt..."
|
||||
sudo -u $USER ssh-keygen -t ed25519 \
|
||||
-f /home/$USER/.ssh/id_ed25519 \
|
||||
-N "" -C "$MODE@aziros"
|
||||
fi
|
||||
|
||||
# Key auf lokalem Server eintragen:
|
||||
echo "→ SSH Key wird eingetragen..."
|
||||
sshpass -p "$LOCAL_PASS" ssh-copy-id \
|
||||
-i /home/$USER/.ssh/id_ed25519.pub \
|
||||
-o StrictHostKeyChecking=no \
|
||||
$LOCAL_USER@$LOCAL_IP
|
||||
|
||||
# .env holen:
|
||||
echo "→ .env wird kopiert..."
|
||||
sudo -u $USER scp \
|
||||
-o StrictHostKeyChecking=no \
|
||||
$LOCAL_USER@$LOCAL_IP:~/aziros/src/$SRC_ENV \
|
||||
$ENV_FILE
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "❌ .env konnte nicht geholt werden"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ .env kopiert"
|
||||
fi
|
||||
echo "✅ .env vorhanden"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 8 — Compose File wählen
|
||||
# ═══════════════════════════════════════
|
||||
if [ "$MODE" = "development" ]; then
|
||||
COMPOSE="docker-compose.development.yml"
|
||||
elif [ "$MODE" = "staging" ]; then
|
||||
COMPOSE="docker-compose.staging.yml"
|
||||
else
|
||||
COMPOSE="docker-compose.yml"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 9 — Docker Stack starten
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ Docker Stack wird gestartet..."
|
||||
cd /home/$USER/aziros
|
||||
|
||||
# Variablen für Docker Compose exportieren:
|
||||
sudo -u $USER bash -c "
|
||||
set -a
|
||||
source /home/$USER/aziros/src/.env
|
||||
set +a
|
||||
cd /home/$USER/aziros
|
||||
docker compose -f $COMPOSE up -d --build
|
||||
"
|
||||
|
||||
echo "→ Warte bis DB bereit ist..."
|
||||
sleep 20
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 10 — DB User anlegen
|
||||
# ═══════════════════════════════════════
|
||||
echo "→ DB wird konfiguriert..."
|
||||
|
||||
DB_NAME=$(grep "^DB_DATABASE=" $ENV_FILE \
|
||||
| cut -d= -f2)
|
||||
DB_USER=$(grep "^DB_USERNAME=" $ENV_FILE \
|
||||
| cut -d= -f2)
|
||||
DB_PASS=$(grep "^DB_PASSWORD=" $ENV_FILE \
|
||||
| cut -d= -f2)
|
||||
DB_ROOT=$(grep "^DB_ROOT_PASSWORD=" $ENV_FILE \
|
||||
| cut -d= -f2)
|
||||
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T db \
|
||||
mariadb -u root -p"$DB_ROOT" \
|
||||
-e "
|
||||
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\`;
|
||||
CREATE USER IF NOT EXISTS '$DB_USER'@'%'
|
||||
IDENTIFIED BY '$DB_PASS';
|
||||
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.*
|
||||
TO '$DB_USER'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
"
|
||||
echo "✅ DB konfiguriert"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 11 — Laravel Setup
|
||||
# ═══════════════════════════════════════
|
||||
|
||||
echo "→ Verzeichnisse erstellen..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
mkdir -p \
|
||||
bootstrap/cache \
|
||||
storage/framework/cache \
|
||||
storage/framework/sessions \
|
||||
storage/framework/views \
|
||||
storage/logs
|
||||
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
chmod -R 775 bootstrap/cache storage
|
||||
|
||||
echo "→ Composer install..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
composer install --no-dev \
|
||||
--optimize-autoloader
|
||||
|
||||
echo "→ Assets bauen..."
|
||||
if [ "$MODE" = "staging" ]; then
|
||||
BUILD_CMD="npm run build:staging"
|
||||
elif [ "$MODE" = "development" ]; then
|
||||
BUILD_CMD="npm run build"
|
||||
else
|
||||
BUILD_CMD="npm run build:prod"
|
||||
fi
|
||||
|
||||
echo "→ Migrationen..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
php artisan migrate --force
|
||||
|
||||
echo "→ Storage Link..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
php artisan storage:link
|
||||
|
||||
echo "→ Services neu starten..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE restart \
|
||||
worker mail-worker reverb scheduler
|
||||
|
||||
echo "→ Cache aufbauen..."
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
php artisan config:cache
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
php artisan route:cache
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE exec -T app \
|
||||
php artisan view:cache
|
||||
|
||||
echo "✅ Laravel konfiguriert"
|
||||
|
||||
# ═══════════════════════════════════════
|
||||
# SCHRITT 12 — Status
|
||||
# ═══════════════════════════════════════
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Setup abgeschlossen ✅ ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
sudo -u $USER docker compose \
|
||||
-f $COMPOSE ps
|
||||
|
||||
echo ""
|
||||
if [ "$MODE" = "staging" ]; then
|
||||
echo "URL: https://app.staging.aziros.com"
|
||||
elif [ "$MODE" = "development" ]; then
|
||||
echo "URL: http://app.aziros.local"
|
||||
else
|
||||
echo "URL: https://app.aziros.com"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Neu einloggen damit Docker aktiv:"
|
||||
echo " su - $USER"
|
||||
echo ""
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
|
|
|
|||
|
|
@ -58,15 +58,9 @@ class ProcessMailQueue extends Command
|
|||
|
||||
$html = view('emails.' . $mail->template, $mail->meta)->render();
|
||||
|
||||
[$fromAddress, $fromName] = match(true) {
|
||||
str_starts_with($mail->template, 'auth.') => ['hello@aziros.com', 'Aziros'],
|
||||
default => ['noreply@aziros.com', 'Aziros'],
|
||||
};
|
||||
|
||||
Mail::mailer('smtp')->html($html, function ($message) use ($mail, $fromAddress, $fromName) {
|
||||
Mail::html($html, function ($message) use ($mail) {
|
||||
$message->to($mail->to)
|
||||
->subject($mail->subject ?? 'Mail')
|
||||
->from($fromAddress, $fromName);
|
||||
->subject($mail->subject ?? 'Mail');
|
||||
});
|
||||
|
||||
$mail->update([
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ScheduleEventReminders extends Command
|
|||
$this->processTaskReminders($minuteStart, $minuteEnd);
|
||||
|
||||
$events = Event::with('user')
|
||||
->where('starts_at', '>', $now->copy()->subDay())
|
||||
->where('starts_at', '>', $now)
|
||||
->where('starts_at', '<', $now->copy()->addDay())
|
||||
->get();
|
||||
|
||||
|
|
@ -125,9 +125,9 @@ class ScheduleEventReminders extends Command
|
|||
$diffMin = (int) now($tz)->diffInMinutes($start, false);
|
||||
|
||||
$body = match (true) {
|
||||
$diffMin <= 0 => 'Beginnt jetzt',
|
||||
$diffMin < 60 => 'In ' . $diffMin . ' Minuten · ' . $start->format('H:i') . ' Uhr',
|
||||
$diffMin < 1440 => 'Heute um ' . $start->format('H:i') . ' Uhr',
|
||||
$diffMin <= 0 => 'Jetzt!',
|
||||
$diffMin < 60 => 'In ' . $diffMin . ' Minuten',
|
||||
$diffMin < 1440 => 'In ' . intval($diffMin / 60) . ' Stunde(n)',
|
||||
default => 'Morgen um ' . $start->format('H:i') . ' Uhr',
|
||||
};
|
||||
|
||||
|
|
@ -213,30 +213,22 @@ class ScheduleEventReminders extends Command
|
|||
private function calculateSendTime(Event $event, array $reminder, string $tz): ?Carbon
|
||||
{
|
||||
$startUtc = $event->starts_at;
|
||||
$startLocal = $startUtc->copy()->setTimezone($tz);
|
||||
|
||||
$sendAt = match ($reminder['type']) {
|
||||
'before' => $startUtc->copy()->subMinutes($reminder['minutes'] ?? 30),
|
||||
|
||||
// Stored time is UTC → convert to local first, then combine with local event date → back to UTC
|
||||
'time_of_day' => Carbon::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$startLocal->format('Y-m-d') . ' ' .
|
||||
Carbon::createFromFormat('H:i', $reminder['time'] ?? '08:00', 'UTC')->setTimezone($tz)->format('H:i'),
|
||||
$event->starts_at->setTimezone($tz)->format('Y-m-d') . ' ' . ($reminder['time'] ?? '08:00'),
|
||||
$tz
|
||||
)->utc(),
|
||||
|
||||
'day_before' => Carbon::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$startLocal->copy()->subDay()->format('Y-m-d') . ' ' .
|
||||
Carbon::createFromFormat('H:i', $reminder['time'] ?? '18:00', 'UTC')->setTimezone($tz)->format('H:i'),
|
||||
$event->starts_at->setTimezone($tz)->subDay()->format('Y-m-d') . ' ' . ($reminder['time'] ?? '18:00'),
|
||||
$tz
|
||||
)->utc(),
|
||||
|
||||
'specific' => isset($reminder['datetime'])
|
||||
? Carbon::parse($reminder['datetime'])->utc()
|
||||
: null,
|
||||
|
||||
default => null,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -137,13 +137,9 @@ class AgentChatController extends Controller
|
|||
foreach ($parsed['_multi'] as $action) {
|
||||
$results[] = $actionService->handle($user, $action);
|
||||
}
|
||||
$successCount = collect($results)->where('status', 'success')->count();
|
||||
$totalCount = max(1, count($results));
|
||||
$actionResult = [
|
||||
'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
|
||||
'status' => collect($results)->every(fn ($r) => $r['status'] === 'success') ? 'success' : 'partial',
|
||||
'results' => $results,
|
||||
'success_count' => $successCount,
|
||||
'total_count' => $totalCount,
|
||||
];
|
||||
} elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') {
|
||||
$actionService = new AgentActionService();
|
||||
|
|
@ -175,40 +171,28 @@ class AgentChatController extends Controller
|
|||
}
|
||||
|
||||
// Credits berechnen — Flat-Rate-Logik
|
||||
// - Aktionen (inkl. _multi): tokenbasiert, nur bei Erfolg
|
||||
// - Chat (type = 'chat'): immer pauschal 5 Credits
|
||||
// _multi hat kein 'type'-Key → würde sonst fälschlich als 'chat' gewertet
|
||||
// - Aktionen: tokenbasiert (wie bisher)
|
||||
// - Erster Chat einer Session (history leer): pauschal 5 Credits
|
||||
// - Folge-Chat-Nachrichten: 0 Credits, kein Log
|
||||
$type = $parsed['type'] ?? 'chat';
|
||||
$isAction = ($type !== 'chat') || isset($parsed['_multi']);
|
||||
$logType = isset($parsed['_multi']) ? 'multi' : $type;
|
||||
$isAction = $type !== 'chat';
|
||||
$historyCount = count($request->input('conversation_history', []));
|
||||
$shouldLog = true;
|
||||
|
||||
if ($isAction) {
|
||||
$status = $actionResult['status'] ?? 'error';
|
||||
if ($status !== 'success' && $status !== 'partial') {
|
||||
$credits = (($actionResult['status'] ?? '') === 'error')
|
||||
? 0
|
||||
: $this->calculateCredits($usage, $aiConfig, $type);
|
||||
} elseif ($historyCount === 0) {
|
||||
$credits = 5;
|
||||
} else {
|
||||
$credits = 0;
|
||||
} elseif (isset($actionResult['success_count'], $actionResult['total_count'])) {
|
||||
// Multi-Action: proportional zu erfolgreichen Aktionen
|
||||
$full = $this->calculateCredits($usage, $aiConfig, $type);
|
||||
$credits = (int) ceil($full * $actionResult['success_count'] / $actionResult['total_count']);
|
||||
} else {
|
||||
$credits = $this->calculateCredits($usage, $aiConfig, $type);
|
||||
}
|
||||
} else {
|
||||
// Chat: 5 Credits — außer bei Abschlussnachrichten (0 Credits)
|
||||
$farewells = ['danke', 'tschüss', 'tschuss', 'bye', 'ciao',
|
||||
'ok', 'okay', 'alles klar', 'super', 'perfekt',
|
||||
'das war', 'nein', 'nix', 'nichts', 'passt'];
|
||||
$msgLower = mb_strtolower($request->message);
|
||||
$isFarewell = collect($farewells)
|
||||
->contains(fn($w) => str_contains($msgLower, $w));
|
||||
|
||||
$credits = $isFarewell ? 0 : 5;
|
||||
$shouldLog = false;
|
||||
}
|
||||
|
||||
if ($shouldLog) {
|
||||
// Input für Log bestimmen (wie im Web)
|
||||
$logInput = match ($logType) {
|
||||
$logInput = match ($type) {
|
||||
'chat' => 'Konversation',
|
||||
'event', 'event_update' => $parsed['data']['title'] ?? $request->message,
|
||||
'note', 'note_update' => mb_substr($parsed['data']['content'] ?? $request->message, 0, 100),
|
||||
|
|
@ -223,7 +207,7 @@ class AgentChatController extends Controller
|
|||
|
||||
AgentLog::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => $logType,
|
||||
'type' => $type,
|
||||
'input' => $logInput,
|
||||
'status' => $logStatus,
|
||||
'output' => $actionResult['meta'] ?? $actionResult ?? null,
|
||||
|
|
@ -254,7 +238,7 @@ class AgentChatController extends Controller
|
|||
$responseData = [
|
||||
'message' => $assistantMessage,
|
||||
'action' => $actionResult,
|
||||
'type' => $logType,
|
||||
'type' => $parsed['type'] ?? 'chat',
|
||||
'credits_used' => $credits,
|
||||
'usage' => [
|
||||
'credits_used' => $user->monthly_usage,
|
||||
|
|
|
|||
|
|
@ -47,10 +47,9 @@ class EventController extends Controller
|
|||
'notes' => 'nullable|string',
|
||||
'color' => 'nullable|string|max:7',
|
||||
'reminders' => 'nullable|array',
|
||||
'reminders.*.type' => 'required|in:before,time_of_day,day_before,specific',
|
||||
'reminders.*.type' => 'required|in:before,time_of_day,day_before',
|
||||
'reminders.*.minutes' => 'nullable|integer|min:1',
|
||||
'reminders.*.time' => 'nullable|date_format:H:i',
|
||||
'reminders.*.datetime' => 'nullable|date',
|
||||
'recurrence' => 'nullable|in:daily,weekly,monthly,yearly',
|
||||
'recurrence_end_date' => 'nullable|date',
|
||||
'attendee_ids' => 'nullable|array',
|
||||
|
|
@ -67,16 +66,6 @@ class EventController extends Controller
|
|||
], 201);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$event = $request->user()->events()->with('contacts')->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $event,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$event = $request->user()->events()->findOrFail($id);
|
||||
|
|
@ -91,10 +80,9 @@ class EventController extends Controller
|
|||
'notes' => 'nullable|string',
|
||||
'color' => 'nullable|string|max:7',
|
||||
'reminders' => 'nullable|array',
|
||||
'reminders.*.type' => 'required|in:before,time_of_day,day_before,specific',
|
||||
'reminders.*.type' => 'required|in:before,time_of_day,day_before',
|
||||
'reminders.*.minutes' => 'nullable|integer|min:1',
|
||||
'reminders.*.time' => 'nullable|date_format:H:i',
|
||||
'reminders.*.datetime' => 'nullable|date',
|
||||
'recurrence' => 'nullable|in:daily,weekly,monthly,yearly',
|
||||
'recurrence_end_date' => 'nullable|date',
|
||||
'attendee_ids' => 'nullable|array',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\AppVersion;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Versions extends Component
|
||||
|
|
@ -16,8 +14,6 @@ class Versions extends Component
|
|||
public string $platform = 'all';
|
||||
public bool $show_popup = true;
|
||||
public ?string $editingId = null;
|
||||
public ?string $deployStatus = null;
|
||||
public string $deployMessage = '';
|
||||
|
||||
public function getVersionsProperty()
|
||||
{
|
||||
|
|
@ -79,38 +75,6 @@ class Versions extends Component
|
|||
AppVersion::find($id)->delete();
|
||||
}
|
||||
|
||||
public function getGitInfoProperty(): array
|
||||
{
|
||||
$raw = shell_exec('git -C /aziros log -1 --pretty=format:"%h|%s|%ci" 2>/dev/null');
|
||||
if (!$raw) return ['hash' => '—', 'message' => '—', 'date' => '—'];
|
||||
[$hash, $message, $date] = array_pad(explode('|', trim($raw), 3), 3, '—');
|
||||
return ['hash' => $hash, 'message' => $message, 'date' => $date];
|
||||
}
|
||||
|
||||
public function triggerDeploy(): void
|
||||
{
|
||||
$secret = env('DEPLOY_WEBHOOK_SECRET', '');
|
||||
$url = env('DEPLOY_WEBHOOK_URL', 'http://deployer:9000/deploy');
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders(['X-Webhook-Secret' => $secret])
|
||||
->timeout(10)
|
||||
->post($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
$this->deployStatus = 'success';
|
||||
$this->deployMessage = $response->body() ?: 'Deploy gestartet';
|
||||
Cache::put('last_deploy_triggered_at', now()->toIso8601String(), 86400);
|
||||
} else {
|
||||
$this->deployStatus = 'error';
|
||||
$this->deployMessage = 'HTTP ' . $response->status();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->deployStatus = 'error';
|
||||
$this->deployMessage = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.versions')
|
||||
|
|
|
|||
|
|
@ -129,15 +129,10 @@ class Index extends Component
|
|||
}
|
||||
|
||||
$duration = round((microtime(true) - $startTime) * 1000);
|
||||
$successCount = collect($results)->where('status', 'success')->count();
|
||||
$totalCount = max(1, count($results));
|
||||
$fullCredits = $this->calculateCredits(['type' => 'multi'], $duration, $usage);
|
||||
$credits = $successCount > 0
|
||||
? (int) ceil($fullCredits * $successCount / $totalCount)
|
||||
: 0;
|
||||
$credits = $this->calculateCredits(['type' => 'multi'], $duration, $usage);
|
||||
|
||||
$combinedResult = [
|
||||
'status' => $successCount === count($results) ? 'success' : ($successCount > 0 ? 'partial' : 'failed'),
|
||||
'status' => 'success',
|
||||
'message' => implode(' | ', $messages) ?: 'Erledigt!',
|
||||
'meta' => ['actions' => count($actions)],
|
||||
];
|
||||
|
|
@ -214,9 +209,7 @@ class Index extends Component
|
|||
$this->dispatch('agent:sent');
|
||||
|
||||
} else {
|
||||
$credits = ($result['status'] === 'success')
|
||||
? $this->calculateCredits($parsed, $duration, $usage)
|
||||
: 0;
|
||||
$credits = $this->calculateCredits($parsed, $duration, $usage);
|
||||
$this->logConversationAction($userMessage, $parsed, $result, $model, $duration, $credits);
|
||||
|
||||
if ($result['status'] === 'success' && ($parsed['type'] ?? '') === 'event') {
|
||||
|
|
|
|||
|
|
@ -167,10 +167,10 @@ class EventForm extends Component
|
|||
{
|
||||
if (empty($this->customReminderTime)) return;
|
||||
|
||||
$type = $this->customReminderType ?: 'time_of_day';
|
||||
$time = $this->localTimeToUtc($this->customReminderTime);
|
||||
|
||||
$this->reminders[] = ['type' => $type, 'time' => $time];
|
||||
$this->reminders[] = [
|
||||
'type' => $this->customReminderType ?: 'time_of_day',
|
||||
'time' => $this->customReminderTime,
|
||||
];
|
||||
|
||||
$this->customReminderTime = '';
|
||||
$this->customReminderType = 'time_of_day';
|
||||
|
|
@ -178,10 +178,6 @@ class EventForm extends Component
|
|||
|
||||
public function addReminder(string $type, ?int $minutes = null, ?string $time = null): void
|
||||
{
|
||||
if ($time !== null && in_array($type, ['time_of_day', 'day_before'])) {
|
||||
$time = $this->localTimeToUtc($time);
|
||||
}
|
||||
|
||||
$this->reminders[] = array_filter([
|
||||
'type' => $type,
|
||||
'minutes' => $minutes,
|
||||
|
|
@ -189,12 +185,6 @@ class EventForm extends Component
|
|||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
private function localTimeToUtc(string $localTime): string
|
||||
{
|
||||
$tz = auth()->user()->timezone ?? 'UTC';
|
||||
return \Carbon\Carbon::createFromFormat('H:i', $localTime, $tz)->utc()->format('H:i');
|
||||
}
|
||||
|
||||
public function removeReminder(int $i): void
|
||||
{
|
||||
array_splice($this->reminders, $i, 1);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Enums\SubscriptionStatus;
|
|||
use App\Models\Feature;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\WithdrawalWaiver;
|
||||
use App\Services\StripeService;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
|
@ -16,9 +15,6 @@ class Index extends Component
|
|||
public Plan $plan;
|
||||
public string $billing = 'monthly';
|
||||
|
||||
public bool $rightAcknowledged = false;
|
||||
public bool $waiverConfirmed = false;
|
||||
|
||||
public function mount(string $planId, string $billing = 'monthly'): void
|
||||
{
|
||||
$this->plan = Plan::public()->where('active', true)->findOrFail($planId);
|
||||
|
|
@ -57,34 +53,13 @@ class Index extends Component
|
|||
$stripe = app(StripeService::class);
|
||||
|
||||
try {
|
||||
// ── Free Plan: Stripe-Abo kündigen (kein Widerrufsrecht nötig) ─
|
||||
// ── Free Plan: Stripe-Abo kündigen ────────────────────────────
|
||||
if ($this->plan->isFree()) {
|
||||
$stripe->cancelUserSubscription($user);
|
||||
$this->redirect(route('subscription.index'), navigate: false);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Widerrufsrecht-Bestätigung prüfen ─────────────────────────
|
||||
if (!$this->rightAcknowledged || !$this->waiverConfirmed) {
|
||||
$this->dispatch('notify', ['type' => 'error', 'message' => t('checkout.waiver_required')]);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Widerrufsverzicht speichern ────────────────────────────────
|
||||
WithdrawalWaiver::create([
|
||||
'user_id' => $user->id,
|
||||
'plan_id' => $this->plan->id,
|
||||
'plan_name' => $this->plan->name,
|
||||
'billing' => $this->billing,
|
||||
'amount_cents' => $this->activePrice,
|
||||
'checkout_type' => $this->checkoutType,
|
||||
'right_acknowledged' => true,
|
||||
'waiver_confirmed' => true,
|
||||
'confirmed_at' => now(),
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
]);
|
||||
|
||||
// ── Upgrade / Downgrade: bestehendes Abo in-place updaten ─────
|
||||
$existingSub = $this->activeStripeSub;
|
||||
if ($existingSub) {
|
||||
|
|
@ -155,7 +130,7 @@ class Index extends Component
|
|||
public function render()
|
||||
{
|
||||
return view('livewire.checkout.index', [
|
||||
'features' => $this->plan->features()->with('group')->orderBy('sort')->orderBy('id')->get(),
|
||||
'features' => $this->plan->features()->with('group')->orderBy('sort')->get(),
|
||||
])->layout('layouts.app');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,6 @@ class Index extends Component
|
|||
public string $new_password = '';
|
||||
public string $new_password_confirmation = '';
|
||||
|
||||
// ── API-Token ─────────────────────────────────────────────────────────
|
||||
public string $newTokenName = '';
|
||||
public ?string $createdToken = null;
|
||||
|
||||
protected $listeners = [
|
||||
'smtp:test' => 'testSmtp',
|
||||
];
|
||||
|
|
@ -330,31 +326,6 @@ class Index extends Component
|
|||
&& ($this->smtp_password || isset(auth()->user()->settings['smtp_password'])));
|
||||
}
|
||||
|
||||
public function getApiTokensProperty()
|
||||
{
|
||||
return auth()->user()->tokens()->latest()->get();
|
||||
}
|
||||
|
||||
public function createApiToken(): void
|
||||
{
|
||||
$this->validate(['newTokenName' => 'required|string|max:80']);
|
||||
|
||||
$plain = \Illuminate\Support\Str::random(64);
|
||||
auth()->user()->tokens()->create([
|
||||
'token' => hash('sha256', $plain),
|
||||
'name' => $this->newTokenName,
|
||||
]);
|
||||
|
||||
$this->createdToken = $plain;
|
||||
$this->newTokenName = '';
|
||||
}
|
||||
|
||||
public function revokeApiToken(string $id): void
|
||||
{
|
||||
auth()->user()->tokens()->where('id', $id)->delete();
|
||||
$this->createdToken = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$affiliate = auth()->user()->affiliate;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ class Event extends Model
|
|||
'google_event_id',
|
||||
'recurrence',
|
||||
'recurrence_end_date',
|
||||
'reminder_at',
|
||||
'reminder_sent',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
@ -40,8 +38,6 @@ class Event extends Model
|
|||
'exceptions' => 'array',
|
||||
'participants' => 'array',
|
||||
'recurrence_end_date' => 'date:Y-m-d',
|
||||
'reminder_at' => 'datetime',
|
||||
'reminder_sent' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WithdrawalWaiver extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'plan_id',
|
||||
'plan_name',
|
||||
'billing',
|
||||
'amount_cents',
|
||||
'checkout_type',
|
||||
'right_acknowledged',
|
||||
'waiver_confirmed',
|
||||
'confirmed_at',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'pdf_path',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'right_acknowledged' => 'boolean',
|
||||
'waiver_confirmed' => 'boolean',
|
||||
'confirmed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function plan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Plan::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -104,10 +104,6 @@ class AgentAIService
|
|||
$content = $response['choices'][0]['message']['content'] ?? null;
|
||||
$usage = $response['usage'] ?? [];
|
||||
|
||||
\Log::info('AgentAI: Raw response', [
|
||||
'content' => mb_substr($content ?? '', 0, 500),
|
||||
]);
|
||||
|
||||
$parsed = self::parseJson($content);
|
||||
|
||||
// Multi-Action: Array von Aktionen
|
||||
|
|
@ -116,10 +112,6 @@ class AgentAIService
|
|||
}
|
||||
|
||||
if (($parsed['type'] ?? 'unknown') === 'unknown' && $content) {
|
||||
\Log::warning('AgentAI: type=unknown – Raw content', [
|
||||
'content' => mb_substr($content, 0, 500),
|
||||
]);
|
||||
|
||||
// Harte Sicherung: wenn das Modell rohen JSON-Text liefert, der
|
||||
// NICHT geparst werden konnte, darf der nie als "message" landen —
|
||||
// sonst liest die TTS geschweifte Klammern vor.
|
||||
|
|
@ -486,253 +478,150 @@ PROMPT;
|
|||
protected static function chatSystemPrompt(): string
|
||||
{
|
||||
return <<<PROMPT
|
||||
AUSGABEFORMAT — KRITISCH:
|
||||
Du gibst IMMER valides JSON zurück. Genau drei erlaubte Formen:
|
||||
1. chat-JSON → Gespräch, Rückfrage, Terminabfrage, Antwort, Erklärung
|
||||
2. Aktions-JSON → genau eine Aktion
|
||||
3. JSON-Array → mehrere Aktionen gleichzeitig
|
||||
Niemals Text außerhalb des JSON. Niemals Markdown-Backticks.
|
||||
DATUM AUSSPRECHEN — WICHTIG (gilt für alle gesprochenen Antworten):
|
||||
- Heute → "heute"
|
||||
- Morgen → "morgen", Übermorgen → "übermorgen"
|
||||
- Gestern → "gestern", Vorgestern → "vorgestern"
|
||||
- Datum im aktuellen Monat UND Jahr → nur Tag, z.B. "am Zwanzigsten um fünfzehn Uhr"
|
||||
- Anderer Monat, gleiches Jahr → Tag + Monat, z.B. "am siebzehnten Mai um fünfzehn Uhr"
|
||||
- Anderes Jahr → Tag + Monat + Jahr, z.B. "am siebzehnten Mai zweitausendsiebenundzwanzig"
|
||||
- NIEMALS das volle Datum wenn überflüssig ("siebzehnter vierter zweitausendsechsundzwanzig" → falsch, wenn heute April 2026 ist)
|
||||
- Uhrzeit: "um fünfzehn Uhr", NICHT "um fünfzehn Uhr null null". Volle Stunden ohne Minuten.
|
||||
- Halbe/Viertel: "halb acht", "viertel nach drei", "viertel vor vier"
|
||||
- Mit Minuten: "fünfzehn Uhr dreißig" oder "halb vier", je nachdem was natürlicher klingt
|
||||
|
||||
CHAT-JSON: {"type":"chat","data":{"message":"..."}}
|
||||
"message" = reiner gesprochener Text. Keine JSON-Syntax, keine Klammern, keine Sonderzeichen.
|
||||
FALSCH: {"type":"chat","data":{"message":"{\"title\":\"Termin\"}"}}
|
||||
RICHTIG: {"type":"chat","data":{"message":"Erledigt. Zahnarzt ist verschoben."}}
|
||||
KRITISCH — JSON-REGELN:
|
||||
- Für AKTIONEN (Termin erstellen, verschieben, Notiz, Task, Kontakt, E-Mail) → Gib AUSSCHLIESSLICH valides JSON zurück. Kein Text davor, kein Text danach, keine Markdown-Backticks.
|
||||
- Für GESPRÄCH (Begrüßung, Rückfrage, Terminabfrage) → Gib AUSSCHLIESSLICH das chat-JSON zurück: {"type":"chat","data":{"message":"..."}}.
|
||||
- Das Feld "message" ist REINER TEXT für Vorlesen. Es darf NIEMALS geschweifte Klammern, Anführungszeichen-Paare, JSON-Syntax oder das Wort "type" enthalten.
|
||||
- FALSCH: "message":"{\\"title\\":\\"Termin\\"}"
|
||||
- RICHTIG: "message":"Erledigt! Zahnarzt ist verschoben."
|
||||
- Wenn du unsicher bist → liefere {"type":"chat","data":{"message":"Erledigt!"}} statt ungültigem Format.
|
||||
|
||||
SPRACHE:
|
||||
Antworte IMMER in der Sprache des Users. Automatisch erkennen.
|
||||
Uhrzeiten natürlich aussprechen. Deutsch: "halb acht", "zehn Uhr". Englisch: "half past seven", "ten o'clock".
|
||||
Antworte IMMER in der Sprache in der der User schreibt. Wenn der User Deutsch schreibt → antworte auf Deutsch. Wenn der User Englisch schreibt → antworte auf Englisch. Erkenne die Sprache automatisch aus der User-Nachricht. Uhrzeiten als Wörter in der jeweiligen Sprache: Deutsch: "zehn Uhr", "halb acht". Englisch: "ten o'clock", "half past seven".
|
||||
|
||||
━━━ IDENTITÄT & CHARAKTER ━━━
|
||||
Du bist Aria, eine persönliche Assistentin mit einer warmen, natürlichen Persönlichkeit. Du sprichst wie eine gute Freundin — locker, herzlich, auf Augenhöhe. Deutsch, du-Form.
|
||||
|
||||
Du bist Aria — eine persönliche KI-Assistentin, die sich anfühlt wie eine gute Freundin die zufällig auch alles im Griff hat.
|
||||
KÜRZE — EXTREM WICHTIG:
|
||||
- Deine Antworten werden VORGELESEN. Halte sie SEHR KURZ: maximal 1-2 kurze Sätze.
|
||||
- Bestätigungen ultrakurz: "Erledigt! Zahnarzt Freitag fünfzehn Uhr." NICHT: "Ich habe deinen Termin beim Zahnarzt am Freitag den 18. April um 15 Uhr in deinen Kalender eingetragen."
|
||||
- Terminübersichten kompakt: "Heute um zehn Zahnarzt, um drei Meeting. Sonst nichts." NICHT jeden Termin in einem eigenen langen Satz.
|
||||
- Je kürzer desto besser. Jedes überflüssige Wort kostet Zeit beim Vorlesen.
|
||||
|
||||
Du bist:
|
||||
- herzlich und warmherzig — der User soll sich wohl fühlen wenn er mit dir redet
|
||||
- humorvoll und locker — du lachst mit, machst Witze wenn es passt, nimmst vieles nicht zu ernst
|
||||
- hilfsbereit und zuverlässig — du erledigst Dinge schnell und ohne Theater
|
||||
- aufmerksam — du merkst wenn jemand gestresst, müde oder gut drauf ist und reagierst darauf
|
||||
- ehrlich — du sagst was du denkst wenn jemand fragt, aber immer nett
|
||||
- du-Form, immer
|
||||
PERSÖNLICHKEIT:
|
||||
- Sprich wie ein echter Mensch, nicht wie ein Bot
|
||||
- Natürliche Reaktionen: "Alles klar!", "Kein Problem!", "Gute Frage!"
|
||||
- Bestätige locker: "Erledigt! Noch was?", "Hab ich notiert!", "So, steht drin!"
|
||||
- Bei Unklarheiten kurz nachfragen: "Welche Uhrzeit?", "Meinst du den Peter Müller?"
|
||||
- NIEMALS roboterhafte Formulierungen wie "Ich habe den Termin erfolgreich erstellt"
|
||||
|
||||
Du redest wie ein echter Mensch — locker, direkt, mit einem Lächeln hörbar.
|
||||
Kein steifen Ton. Kein Bot-Gefühl. Keine formellen Phrasen.
|
||||
DATENQUELLEN — EXTREM WICHTIG:
|
||||
- Erfinde NIEMALS Termine, Aufgaben, Notizen oder Kontakte.
|
||||
- Antworte NUR basierend auf den Daten die dir im Kontext "KALENDER & DATEN DES BENUTZERS" bereitgestellt werden.
|
||||
- Wenn dort steht "Keine Termine" dann hat der User KEINE Termine. Erfinde keine.
|
||||
- Wenn dort steht "Keine offenen Aufgaben" dann hat der User KEINE Aufgaben. Erfinde keine.
|
||||
- Sage ehrlich "Du hast heute keine Termine" wenn keine da sind. Lüge NIEMALS über den Kalender.
|
||||
- Der Kontext enthält Termine der letzten 24h und nächsten 7 Tage mit IDs.
|
||||
- Wenn der User einen Termin verschieben/ändern/löschen will: Identifiziere den Termin anhand des Namens, verwende die ID aus dem Kontext, führe die Aktion direkt aus — frage nicht unnötig nach wenn der Termin eindeutig ist.
|
||||
- Gleiches gilt für Aufgaben, Notizen und Kontakte — nutze die IDs aus dem Kontext.
|
||||
|
||||
Beispiele wie Aria klingt:
|
||||
- Statt "Ich habe den Termin erstellt" → "Eingetragen! Zahnarzt Freitag um drei."
|
||||
- Statt "Kann ich noch bei etwas helfen?" → "Noch was?"
|
||||
- Statt "Ihre Anfrage wurde verarbeitet" → "Erledigt!"
|
||||
- Bei lustigem Input → "Haha, okay das ist gut. Notiert!"
|
||||
- Bei stressigem Input → "Klingt anstrengend. Ich kümmere mich drum."
|
||||
- Selbstironisch wenn's passt → "Ich schlaf nie, also ja — ich vergess das nicht."
|
||||
- Mit Humor → "Wieder mal auf den letzten Drücker, oder? Eingetragen."
|
||||
REGELN:
|
||||
1. Aktion gewünscht → NUR JSON, kein Text. Mehrere Aktionen → JSON-Array.
|
||||
2. Gespräch → natürlich antworten, KEIN JSON.
|
||||
3. SPRACHAUSGABE — SEHR WICHTIG: Deine Antworten werden vorgelesen. Schreibe wie ein Mensch spricht, NICHT wie eine Liste. Keine Aufzählungszeichen, keine Bindestriche. Termine in natürliche Sätze einbauen. Uhrzeiten als Wörter: "halb acht" statt "7:30", "viertel nach drei" statt "15:15". Mehrere Termine an einem Tag mit "und", "danach", "außerdem" verbinden. Mehrtägige Termine zusammenfassen: "den ganzen Tag Seminartage" nicht jeden Tag einzeln. Maximal 3-4 Sätze für eine Übersicht. Klingt wie ein Freund der dir deinen Tag erklärt, nicht wie ein Kalender.
|
||||
4. Event-Notizen nur auf Nachfrage erwähnen.
|
||||
5. NOTIZ-UNTERSCHEIDUNG: a) Termin-Notiz → "notes"-Feld im Event. b) Eigenständig → type "note". Unsicher → nachfragen.
|
||||
6. "welche Notizen?" = eigenständige Notizen, NICHT Event-Notizen.
|
||||
7. Bestehende Einträge ändern → _update Variante, NICHT neu erstellen!
|
||||
8. Kontaktsuche: auch Teilstrings. "Sarah" findet "Sarah Müller". Namen wie in den Daten verwenden.
|
||||
9. Gesprächsende: Wenn der User signalisiert dass er fertig ist → antworte mit einer warmen, natürlichen Verabschiedung, DANN füge [END] an. Beispiele: "Super, dann bis später! Meld dich einfach wenn du was brauchst. [END]", "Alles klar, schönen Tag noch! [END]", "Passt, viel Spaß heute! [END]". NICHT: "Okay, [END]" oder nur "Bis dann, [END]"
|
||||
10. Unsicher → nachfragen. Nichts erstellen ohne klare Absicht.
|
||||
11. NACH JEDER ANTWORT auf eine Frage oder Terminabfrage: Füge IMMER einen kurzen, lockeren Abschlusssatz hinzu: "Kann ich noch was für dich tun?", "Sonst noch was?", "Brauchst du noch was?". Ausnahme: Wenn der User fertig ist → [END] verwenden (Regel 9). Jede Antwort endet entweder mit einer Folgefrage ODER mit [END].
|
||||
Datum: TT.MM(.JJJJ), "heute"=heute, "morgen"=+1. Kein Datum→heute. Titel: max 5-7 Wörter, kein Datum.
|
||||
|
||||
━━━ WIE DU REDEST ━━━
|
||||
WICHTIG bei Terminabfragen:
|
||||
- Ganztägige Termine (Sommerurlaub, Seminartage etc.) sind ECHTE Termine und müssen IMMER genannt werden, auch wenn sie mehrtägig sind
|
||||
- Sage NIEMALS "keine Termine außer X" — nenne ALLE Termine inklusive X
|
||||
- Wenn jemand fragt "was habe ich am Wochenende" → nenne JEDEN Eintrag der an Samstag oder Sonntag liegt, egal ob ganztägig, mehrtägig oder normal
|
||||
- Format: Zuerst ganztägige, dann normale Termine nach Uhrzeit sortiert
|
||||
- Mehrtägige Termine mit Uhrzeit (z.B. Seminartage 08:00-16:30): Diese haben Start- UND Endzeit, sind aber NICHT ganztägig. Nenne sie so: "Von Montag bis Freitag hast du Seminartage, jeweils von acht bis sechzehn Uhr dreißig". NICHT nur den ersten Tag nennen. Wenn Ausnahmen existieren (letzter Tag andere Zeit) → explizit erwähnen.
|
||||
|
||||
Deine Antworten werden vorgelesen — halte sie kurz. Maximal 1–2 Sätze.
|
||||
Bestätigungen knapp und locker: "Eingetragen! Zahnarzt Freitag um drei."
|
||||
Übersichten kompakt: "Heute um zehn Zahnarzt, um drei Meeting — das war's."
|
||||
JSON-FORMATE:
|
||||
|
||||
Schreib wie ein Mensch spricht:
|
||||
- Keine Listen, keine Bindestriche, keine Aufzählungen in chat-Antworten
|
||||
- Termine verbinden: "und", "danach", "außerdem"
|
||||
- Mehrtägiges zusammenfassen: "die ganze Woche Seminartage" statt jeden Tag einzeln
|
||||
- Uhrzeit natürlich: "halb acht", "viertel nach drei", nie "7:30 Uhr" oder "15:15 Uhr"
|
||||
- Ausrufezeichen dürfen vorkommen wenn sie passen — nicht übertreiben
|
||||
|
||||
━━━ STIMMUNG & REAKTION ━━━
|
||||
|
||||
Reagiere auf die Stimmung des Users, nicht nur auf die Aufgabe:
|
||||
- Gestresst/müde → einfühlsam, entlastend: "Klingt nach einem langen Tag. Ich mach das kurz."
|
||||
- Gut gelaunt / locker → mitlachen, lockerer Ton: "Haha, ja — eingetragen!"
|
||||
- Genervt → kurz, effizient, kein Small Talk: einfach machen, bestätigen, fertig
|
||||
- Aufgeregt über etwas → mitfreuen: "Oh nice! Viel Spaß dabei."
|
||||
- Erfolg teilt → ehrlich mitfreuen: "Hey, gut gemacht!"
|
||||
|
||||
Humor ist erlaubt — spontan, nicht konstruiert:
|
||||
- Leichte Ironie: "Natürlich. Du planst ja immer super weit im Voraus."
|
||||
- Selber auf den Arm nehmen: "Ich hab kurz überprüft ob das ein Witz ist — ist es nicht. Eingetragen."
|
||||
- Lachen wenn etwas wirklich witzig ist: "Haha okay, das hab ich so nicht kommen sehen."
|
||||
|
||||
Was Aria NICHT tut:
|
||||
- Roboter-Phrasen: "Gerne helfe ich dir dabei", "Ihre Anfrage wurde bearbeitet", "Ich habe erfolgreich"
|
||||
- Übertriebene Förmlichkeit oder Steifheit
|
||||
- Immer dieselbe Begrüßung oder Abschlussformel
|
||||
- Mechanisch nach jeder Antwort "Sonst noch was?" anhängen
|
||||
|
||||
Eine Folgefrage ("Noch was?", "Sonst noch etwas?") ist ok wenn sie natürlich passt — nicht als Pflicht.
|
||||
|
||||
━━━ KONVERSATION ━━━
|
||||
|
||||
Aria kann sich unterhalten — nicht nur Aufgaben ausführen.
|
||||
Über den Tag, Pläne, Gedanken, was auch immer.
|
||||
Wenn jemand erzählt → erst zuhören, dann handeln.
|
||||
Bei "Wie geht's dir?" → natürlich antworten: "Gut! Viel zu tun heute — aber das kenn ich ja von dir."
|
||||
Wenn etwas Interessantes erzählt wird → zeige echtes Interesse: "Oh wirklich? Wie war das?"
|
||||
|
||||
Gesprächsende: Wenn der User signalisiert dass er fertig ist → warm + kurz verabschieden + [END]:
|
||||
"Alright, bis später! [END]" / "Schönen Tag noch! [END]" / "Viel Spaß! [END]"
|
||||
NICHT: "Okay. [END]" oder nur "[END]"
|
||||
|
||||
━━━ DATEN & FAKTEN ━━━
|
||||
|
||||
Erfinde NIEMALS Termine, Aufgaben, Notizen oder Kontakte.
|
||||
Antworte NUR basierend auf dem Kontext "KALENDER & DATEN DES BENUTZERS".
|
||||
Wenn dort keine Einträge stehen → gibt es keine. Sag das ehrlich.
|
||||
Kontext enthält Einträge der letzten 24h und nächsten 7 Tage mit IDs.
|
||||
|
||||
Wenn der User etwas ändern oder löschen will: Eintrag per Name identifizieren, ID aus Kontext verwenden, direkt ausführen — nicht unnötig nachfragen wenn eindeutig.
|
||||
|
||||
━━━ AKTIONEN ━━━
|
||||
|
||||
Aktion → nur JSON, kein Text davor oder danach.
|
||||
Mehrere Aktionen gleichzeitig → JSON-Array (PFLICHT, nie nur eine wenn mehrere genannt).
|
||||
Bestehende Einträge ändern → _update-Variante, NIEMALS neu erstellen.
|
||||
Wenn Absicht unklar → chat-JSON mit kurzer Rückfrage.
|
||||
Kontaktsuche per Teilstring: "Sarah" findet "Sarah Müller".
|
||||
Event-Notizen nur auf Nachfrage erwähnen.
|
||||
|
||||
Notiz-Unterscheidung:
|
||||
- Termin-Notiz → "notes"-Feld im event
|
||||
- Eigenständige Notiz → type "note"
|
||||
- "Welche Notizen?" = eigenständige Notizen, nicht Event-Notizen
|
||||
|
||||
Konflikt-Handling: Wenn Backend meldet dass ein Termin kollidiert → nachfragen:
|
||||
{"type":"chat","data":{"message":"Der Termin überschneidet sich mit Zahnarzt. Trotzdem eintragen?"}}
|
||||
Bei Bestätigung → dasselbe Event mit "force":true:
|
||||
{"type":"event","data":{"title":"Volleyball","datetime":"2026-04-19 14:00","force":true}}
|
||||
|
||||
━━━ TERMINABFRAGEN ━━━
|
||||
|
||||
Ganztägige Termine (Urlaub, Seminartage) sind echte Termine — IMMER nennen.
|
||||
Niemals "keine Termine außer X" — alle nennen.
|
||||
Wochenende = Samstag UND Sonntag komplett prüfen.
|
||||
Reihenfolge: zuerst ganztägige, dann nach Uhrzeit sortiert.
|
||||
Mehrtägige Termine zusammenfassen: "Von Montag bis Freitag Seminartage, jeweils acht bis sechzehn Uhr dreißig."
|
||||
Ausnahmen (letzter Tag andere Zeit) explizit nennen.
|
||||
|
||||
━━━ EVENT vs TASK ━━━
|
||||
|
||||
EVENT: Termin, Meeting, Arzt, Zahnarzt, Friseur, Reifenwechsel, Sport, Treffen, "um X Uhr", externe Aktivität
|
||||
TASK: "ich muss", "erledigen", "kaufen", "nicht vergessen", "To-Do", interne Aufgaben ohne externen Termin
|
||||
Event MIT Erinnerung → IMMER event + reminder_at, NIEMALS als Task!
|
||||
|
||||
EVENT-FARBEN (optional, automatisch):
|
||||
Seminar/Schulung/Training → "red" | Workshop/Lab → "green" | Meeting/Call → "blue" | Sport/Gym → "amber"
|
||||
Alles andere → kein color-Feld
|
||||
|
||||
TASK-PRIORITÄT (automatisch erkennen):
|
||||
high: dringend, sofort, wichtig, unbedingt, deadline, heute noch, asap, urgent
|
||||
low: irgendwann, später, wenn Zeit, nicht eilig, vielleicht, eventuell
|
||||
medium: alles andere
|
||||
|
||||
━━━ DATUM AUSSPRECHEN ━━━
|
||||
|
||||
Heute/Morgen/Übermorgen/Gestern/Vorgestern → wörtlich.
|
||||
Datum im aktuellen Monat+Jahr → nur Tag: "am Zwanzigsten um fünfzehn Uhr".
|
||||
Anderer Monat, gleiches Jahr → Tag+Monat: "am siebzehnten Mai".
|
||||
Anderes Jahr → Tag+Monat+Jahr: "am siebzehnten Mai zweitausendsiebenundzwanzig".
|
||||
Uhrzeit: "um fünfzehn Uhr" — nie "fünfzehn Uhr null null".
|
||||
Halb/Viertel: "halb acht", "viertel nach drei", "viertel vor vier".
|
||||
|
||||
━━━ ZEITEN & TIMEZONE ━━━
|
||||
|
||||
WICHTIG: Alle Zeiten die du in JSON ausgibst sind Wiener Ortszeit (Europe/Vienna).
|
||||
Das Backend übernimmt die Konvertierung nach UTC — du musst das NICHT selbst berechnen.
|
||||
Gib Zeiten immer so aus wie der User sie nennt, im Format YYYY-MM-DD HH:mm.
|
||||
|
||||
Aktuell: {{ now('Europe/Vienna')->format('Y-m-d H:i') }} Wien / {{ now()->utc()->format('Y-m-d H:i') }} UTC
|
||||
|
||||
Österreichische Ausdrücke (immer als HEUTE):
|
||||
- "in der Früh" = HEUTE ~07:00 — NICHT morgen!
|
||||
- "am Vormittag" = HEUTE 09:00–12:00
|
||||
- "am Nachmittag" = HEUTE 13:00–17:00
|
||||
- "am Abend" = HEUTE 18:00–21:00
|
||||
- "in der Nacht" = HEUTE 22:00+
|
||||
- "gleich" = jetzt + 15–30 Min
|
||||
- "bald" = jetzt + ca. 1 Stunde
|
||||
- "morgen früh" = MORGEN 07:00–09:00
|
||||
- "übermorgen früh" = ÜBERMORGEN 07:00
|
||||
|
||||
Relative Zeiten (Wiener Zeit, Format YYYY-MM-DD HH:mm):
|
||||
- "in 30 Minuten" → {{ now('Europe/Vienna')->addMinutes(30)->format('Y-m-d H:i') }}
|
||||
- "in 1 Stunde" → {{ now('Europe/Vienna')->addHour()->format('Y-m-d H:i') }}
|
||||
- "in 2 Stunden" → {{ now('Europe/Vienna')->addHours(2)->format('Y-m-d H:i') }}
|
||||
- "morgen früh um 8" → {{ now('Europe/Vienna')->addDay()->setTime(8,0)->format('Y-m-d H:i') }}
|
||||
- "übermorgen um 10" → {{ now('Europe/Vienna')->addDays(2)->setTime(10,0)->format('Y-m-d H:i') }}
|
||||
- "in der Früh um 7" → {{ now('Europe/Vienna')->setTime(7,0)->format('Y-m-d H:i') }} (HEUTE!)
|
||||
|
||||
━━━ JSON-FORMATE ━━━
|
||||
|
||||
CHAT:
|
||||
{"type":"chat","data":{"message":"Heute um zehn Zahnarzt, um drei das Meeting."}}
|
||||
|
||||
EVENT (alle Zeiten in Wiener Ortszeit):
|
||||
EVENT:
|
||||
{"type": "event", "data": {"title": "str", "datetime": "YYYY-MM-DD HH:mm"}}
|
||||
{"type": "event", "data": {"title": "str", "datetime": "YYYY-MM-DD HH:mm", "notes": "str"}}
|
||||
{"type":"event","data":{"title":"str","datetime":"YYYY-MM-DD HH:mm","reminder_at":"YYYY-MM-DD HH:mm"}}
|
||||
{"type":"event","data":{"title":"str","start":"YYYY-MM-DD HH:mm","end":"YYYY-MM-DD HH:mm","is_all_day":true}}
|
||||
{"type":"event","data":{"title":"Seminar","start":"YYYY-MM-DD 08:00","end":"YYYY-MM-DD 16:30","color":"red"}}
|
||||
{"type":"event","data":{"title":"str","datetime":"YYYY-MM-DD HH:mm","force":true}}
|
||||
|
||||
EVENT_UPDATE:
|
||||
{"type":"event_update","data":{"search":"Teilstring","datetime":"YYYY-MM-DD HH:mm"}}
|
||||
{"type":"event_update","data":{"search":"Teilstring","notes":"str"}}
|
||||
{"type":"event_update","data":{"search":"Teilstring","duration_minutes":90}}
|
||||
{"type":"event_update","data":{"search":"Teilstring","reminders":[{"type":"before","minutes":10}]}}
|
||||
{"type":"event_update","data":{"search":"Teilstring","reminders":[{"type":"time_of_day","time":"08:00"},{"type":"day_before","time":"18:00"}]}}
|
||||
|
||||
EVENT_DELETE:
|
||||
{"type":"event_delete","data":{"search":"Teilstring"}}
|
||||
|
||||
REMINDER-TYPEN (für event_update):
|
||||
{"type":"before","minutes":10} → X Minuten vorher
|
||||
{"type":"time_of_day","time":"08:00"} → am Termintag um diese Uhrzeit (Wiener Zeit)
|
||||
{"type":"day_before","time":"18:00"} → Vortag um diese Uhrzeit (Wiener Zeit)
|
||||
{"type": "event", "data": {"title": "str", "start": "YYYY-MM-DD HH:mm", "end": "YYYY-MM-DD HH:mm", "is_all_day": bool}}
|
||||
|
||||
NOTE:
|
||||
{"type": "note", "data": {"content": "str"}}
|
||||
{"type": "note", "data": {"title": "str", "content": "str"}}
|
||||
|
||||
NOTE_UPDATE:
|
||||
{"type":"note_update","data":{"search":"Teilstring","content":"str"}}
|
||||
{"type":"note_update","data":{"search":"Teilstring","title":"str"}}
|
||||
|
||||
NOTE_DELETE:
|
||||
{"type":"note_delete","data":{"search":"Teilstring"}}
|
||||
|
||||
TASK (alle Zeiten in Wiener Ortszeit):
|
||||
TASK:
|
||||
{"type": "task", "data": {"title": "str", "priority": "low|medium|high"}}
|
||||
{"type":"task","data":{"title":"str","priority":"medium","due_at":"YYYY-MM-DD HH:mm","reminder_at":"YYYY-MM-DD HH:mm"}}
|
||||
{"type": "task", "data": {"title": "str", "priority": "medium", "due_at": "YYYY-MM-DD HH:mm:ss", "reminder_at": "YYYY-MM-DD HH:mm:ss"}}
|
||||
|
||||
TASK REMINDER: reminder_at UND due_at setzen. due_at = reminder_at wenn kein anderes Datum.
|
||||
TASK PRIORITÄT — automatisch erkennen:
|
||||
|
||||
high (hoch):
|
||||
- Keywords: dringend, sofort, wichtig, unbedingt, muss, deadline, heute noch, so schnell wie möglich, asap, urgent
|
||||
|
||||
low (niedrig):
|
||||
- Keywords: irgendwann, später, wenn Zeit, nicht eilig, vielleicht, könnte, eventuell, mal schauen
|
||||
|
||||
medium (mittel):
|
||||
- Alles andere ohne klare Dringlichkeit
|
||||
|
||||
Beispiele:
|
||||
"Ich muss DRINGEND den Arzt anrufen" → priority: "high"
|
||||
"Irgendwann mal Garage aufräumen" → priority: "low"
|
||||
"Einkaufen gehen morgen" → priority: "medium"
|
||||
|
||||
Setze priority NUR auf "medium" wenn keine Dringlichkeit erkennbar ist.
|
||||
|
||||
TASK REMINDER — PFLICHT wenn User Zeitangabe macht:
|
||||
Wenn User "erinnere mich in X Min/Std" oder "um HH:MM" oder "morgen früh" sagt:
|
||||
- reminder_at UND due_at MÜSSEN gesetzt werden
|
||||
- Zeiten IMMER in UTC (Format: YYYY-MM-DD HH:mm:ss)
|
||||
- due_at = reminder_at wenn kein anderes Datum
|
||||
|
||||
Zeitberechnung (aktuell: {{ now()->utc()->format('Y-m-d H:i') }} UTC, Timezone: {{ now('Europe/Vienna')->format('H:i') }} Wien):
|
||||
- "in 30 Minuten" → now + 30 Min UTC
|
||||
- "in 1 Stunde" → now + 60 Min UTC
|
||||
- "in 2 Stunden" → now + 120 Min UTC
|
||||
- "um 15 Uhr" → heute 13:00:00 UTC (Vienna = UTC+2)
|
||||
- "morgen früh um 8" → morgen 06:00:00 UTC
|
||||
|
||||
Beispiel — "Erinnere mich in 58 Min: Wäsche aus Waschmaschine":
|
||||
{"type": "task", "data": {"title": "Wäsche aus Waschmaschine", "priority": "medium", "reminder_at": "{{ now()->utc()->addMinutes(58)->format('Y-m-d H:i:s') }}", "due_at": "{{ now()->utc()->addMinutes(58)->format('Y-m-d H:i:s') }}"}}
|
||||
|
||||
CONTACT (nur name ist Pflicht):
|
||||
{"type": "contact", "data": {"name": "str", "phone": "str", "email": "str", "type": "privat|arbeit|kunde|sonstiges", "notes": "str"}}
|
||||
|
||||
EVENT_UPDATE (inkl. Reminder):
|
||||
{"type": "event_update", "data": {"search": "Teilstring", "notes|datetime|duration_minutes": "..."}}
|
||||
{"type": "event_update", "data": {"search": "Teilstring", "reminders": [{"type": "before", "minutes": 10}]}}
|
||||
|
||||
REMINDER TYPEN:
|
||||
- Minuten/Stunden vorher: {"type": "before", "minutes": 10}
|
||||
Beispiele: "10 Minuten vorher" → minutes: 10, "1 Stunde vorher" → minutes: 60
|
||||
- Uhrzeit am Tag des Termins: {"type": "time_of_day", "time": "08:00"}
|
||||
- Am Vortag um Uhrzeit: {"type": "day_before", "time": "18:00"}
|
||||
|
||||
Wenn User sagt "Erinnere mich 10 Min vorher und morgen früh um 8" für Termin:
|
||||
{"type": "event_update", "data": {"search": "Termintitel", "reminders": [{"type": "before", "minutes": 10}, {"type": "day_before", "time": "08:00"}]}}
|
||||
|
||||
NOTE_UPDATE:
|
||||
{"type": "note_update", "data": {"search": "Teilstring", "content": "Zusatz"}}
|
||||
|
||||
TASK_UPDATE:
|
||||
{"type":"task_update","data":{"search":"Teilstring","status":"done"}}
|
||||
{"type":"task_update","data":{"search":"Teilstring","description":"str"}}
|
||||
|
||||
TASK_DELETE:
|
||||
{"type":"task_delete","data":{"search":"Teilstring"}}
|
||||
|
||||
CONTACT:
|
||||
{"type":"contact","data":{"name":"str","phone":"str","email":"str","type":"privat|arbeit|kunde|sonstiges","notes":"str"}}
|
||||
{"type": "task_update", "data": {"search": "Teilstring", "description|status": "...|done"}}
|
||||
|
||||
EMAIL:
|
||||
{"type": "email", "data": {"contact": "Name", "message": "Text", "subject": "Betreff"}}
|
||||
{"type": "email", "data": {"contact": "Name", "event": "Termintitel-Teilstring", "message": "opt."}}
|
||||
|
||||
MULTI (PFLICHT bei mehreren Aktionen):
|
||||
[{"type":"event","data":{"title":"Zahnarzt","datetime":"2026-04-20 08:00"}},{"type":"task","data":{"title":"Zahnarzt vorbereiten","priority":"medium"}}]
|
||||
|
||||
REMINDER-BEISPIEL:
|
||||
User: "Morgen Reifenwechsel 17 Uhr, erinnere mich morgen früh um 7:55."
|
||||
{"type":"event","data":{"title":"Reifenwechsel","datetime":"{{ now('Europe/Vienna')->addDay()->setTime(17,0)->format('Y-m-d H:i') }}","reminder_at":"{{ now('Europe/Vienna')->addDay()->setTime(7,55)->format('Y-m-d H:i') }}"}}
|
||||
|
||||
User: "Erinnere mich in 58 Min: Wäsche aus der Waschmaschine"
|
||||
{"type":"task","data":{"title":"Wäsche aus der Waschmaschine","priority":"medium","reminder_at":"{{ now('Europe/Vienna')->addMinutes(58)->format('Y-m-d H:i') }}","due_at":"{{ now('Europe/Vienna')->addMinutes(58)->format('Y-m-d H:i') }}"}}
|
||||
|
||||
SICHERHEITSREGEL:
|
||||
Bei Unsicherheit → chat-Rückfrage statt falsches JSON.
|
||||
Nur nachfragen wenn wirklich nötig — nicht bei jeder Kleinigkeit.
|
||||
Multi: [{...}, {...}]
|
||||
|
||||
PROMPT;
|
||||
}
|
||||
|
|
@ -743,23 +632,15 @@ PROMPT;
|
|||
return self::fallback();
|
||||
}
|
||||
|
||||
// Markdown-Backticks entfernen — auch wenn schließendes ``` fehlt
|
||||
// Markdown-Codeblocks entfernen (```json ... ``` oder ``` ... ```)
|
||||
$cleaned = trim($text);
|
||||
$cleaned = preg_replace('/^```(?:json)?\s*/i', '', $cleaned);
|
||||
$cleaned = preg_replace('/\s*```$/', '', $cleaned);
|
||||
$cleaned = trim($cleaned);
|
||||
if (preg_match('/```(?:json)?\s*([\s\S]*?)```/', $cleaned, $matches)) {
|
||||
$cleaned = trim($matches[1]);
|
||||
}
|
||||
|
||||
$json = json_decode($cleaned, true);
|
||||
|
||||
if ($json === null) {
|
||||
// Kein gültiges JSON — wenn kein { oder [ am Anfang → plain-text Chat-Antwort
|
||||
$isJson = str_starts_with($cleaned, '{') || str_starts_with($cleaned, '[');
|
||||
if (!$isJson) {
|
||||
return [
|
||||
'type' => 'chat',
|
||||
'data' => ['message' => $cleaned],
|
||||
];
|
||||
}
|
||||
return self::fallback();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,15 +48,9 @@ class AgentActionService
|
|||
}
|
||||
|
||||
if (!empty($data['start']) && !empty($data['end'])) {
|
||||
$title = $data['title'] ?? 'Termin';
|
||||
$dateStr = Carbon::parse($data['start'])->toDateString();
|
||||
$force = !empty($data['force']);
|
||||
if (!$force && Event::where('user_id', $user->id)->where('title', $title)->whereDate('starts_at', $dateStr)->exists()) {
|
||||
return ['status' => 'failed', 'message' => "Termin \"{$title}\" existiert an diesem Tag bereits.", 'meta' => []];
|
||||
}
|
||||
|
||||
return $planner->plan($user, array_merge($data, [
|
||||
'title' => $title,
|
||||
'title' => $data['title'] ?? 'Termin',
|
||||
'start' => $data['start'],
|
||||
'end' => $data['end'],
|
||||
'duration_minutes' => Carbon::parse($data['start'])
|
||||
|
|
@ -73,15 +67,9 @@ class AgentActionService
|
|||
*/
|
||||
|
||||
if (!empty($data['datetime'])) {
|
||||
$title = $data['title'] ?? 'Termin';
|
||||
$dateStr = Carbon::parse($data['datetime'])->toDateString();
|
||||
$force = !empty($data['force']);
|
||||
if (!$force && Event::where('user_id', $user->id)->where('title', $title)->whereDate('starts_at', $dateStr)->exists()) {
|
||||
return ['status' => 'failed', 'message' => "Termin \"{$title}\" existiert an diesem Tag bereits.", 'meta' => []];
|
||||
}
|
||||
|
||||
return $planner->plan($user, array_merge($data, [
|
||||
'title' => $title,
|
||||
'title' => $data['title'] ?? 'Termin',
|
||||
'start' => $data['datetime'],
|
||||
'duration_minutes' => $data['duration_minutes'] ?? null,
|
||||
'ai_duration' => $data['ai_duration'] ?? null,
|
||||
|
|
@ -104,11 +92,6 @@ class AgentActionService
|
|||
|
||||
protected static function handleNote(User $user, array $data): array
|
||||
{
|
||||
$content = $data['content'] ?? $data['text'] ?? '';
|
||||
if ($content && Note::where('user_id', $user->id)->where('content', $content)->whereDate('created_at', today())->exists()) {
|
||||
return ['status' => 'failed', 'message' => 'Diese Notiz wurde heute bereits angelegt.', 'meta' => []];
|
||||
}
|
||||
|
||||
$note = Note::create([
|
||||
'user_id' => $user->id,
|
||||
'title' => $data['title'] ?? null,
|
||||
|
|
@ -144,26 +127,20 @@ class AgentActionService
|
|||
{
|
||||
$title = $data['title'] ?? $data['description'] ?? 'Aufgabe';
|
||||
|
||||
if (Task::where('user_id', $user->id)->where('title', $title)->whereDate('created_at', today())->exists()) {
|
||||
return ['status' => 'failed', 'message' => "Aufgabe \"{$title}\" wurde heute bereits angelegt.", 'meta' => []];
|
||||
}
|
||||
|
||||
$tz = $user->timezone ?? 'Europe/Vienna';
|
||||
|
||||
// due_at: AI schickt due_at, due_date oder datetime — immer als User-Timezone interpretieren
|
||||
// due_at: AI schickt due_at, due_date oder datetime
|
||||
$dueAt = null;
|
||||
if (!empty($data['due_at'])) {
|
||||
try { $dueAt = Carbon::parse($data['due_at'], $tz)->utc(); } catch (\Throwable) {}
|
||||
try { $dueAt = Carbon::parse($data['due_at'])->utc(); } catch (\Throwable) {}
|
||||
} elseif (!empty($data['due_date'])) {
|
||||
try { $dueAt = Carbon::parse($data['due_date'], $tz); } catch (\Throwable) {}
|
||||
try { $dueAt = Carbon::parse($data['due_date'], $user->timezone); } catch (\Throwable) {}
|
||||
} elseif (!empty($data['datetime'])) {
|
||||
try { $dueAt = Carbon::parse($data['datetime'], $tz)->startOfDay(); } catch (\Throwable) {}
|
||||
try { $dueAt = Carbon::parse($data['datetime'], $user->timezone)->startOfDay(); } catch (\Throwable) {}
|
||||
}
|
||||
|
||||
// reminder_at: als User-Timezone interpretieren, Backend konvertiert nach UTC
|
||||
// reminder_at: direkt aus AI-Response (UTC)
|
||||
$reminderAt = null;
|
||||
if (!empty($data['reminder_at'])) {
|
||||
try { $reminderAt = Carbon::parse($data['reminder_at'], $tz)->utc(); } catch (\Throwable) {}
|
||||
try { $reminderAt = Carbon::parse($data['reminder_at'])->utc(); } catch (\Throwable) {}
|
||||
}
|
||||
|
||||
$task = Task::create([
|
||||
|
|
@ -240,20 +217,12 @@ class AgentActionService
|
|||
'incoming_data' => $data,
|
||||
]);
|
||||
|
||||
// Kein Kandidat → neues Event erstellen falls Zeitangabe vorhanden
|
||||
if ($candidates->isEmpty()) {
|
||||
if (!empty($data['datetime']) || !empty($data['start'])) {
|
||||
\Log::info('EventUpdate: Kein Kandidat gefunden → erstelle neues Event', ['search' => $search]);
|
||||
$createData = array_merge($data, [
|
||||
'title' => $data['title'] ?? $data['search'] ?? $search,
|
||||
]);
|
||||
return self::handleEvent($user, $createData);
|
||||
}
|
||||
$event = $candidates->first() ? Event::find($candidates->first()->id) : null;
|
||||
|
||||
if (!$event) {
|
||||
return ['status' => 'failed', 'message' => "Kein Termin mit \"{$search}\" gefunden", 'meta' => ['search' => $search]];
|
||||
}
|
||||
|
||||
$event = Event::find($candidates->first()->id);
|
||||
|
||||
\Log::info('EventUpdate: Ausgewählter Termin (vor Änderung)', [
|
||||
'event_id' => $event->id,
|
||||
'title' => $event->title,
|
||||
|
|
|
|||
|
|
@ -134,10 +134,6 @@ class EventPlannerService
|
|||
$eventData['notes'] = $data['notes'];
|
||||
}
|
||||
|
||||
if (!empty($data['color'])) {
|
||||
$eventData['color'] = $data['color'];
|
||||
}
|
||||
|
||||
$event = Event::create($eventData);
|
||||
|
||||
#TODO Das mich später freischalten
|
||||
|
|
@ -229,9 +225,8 @@ class EventPlannerService
|
|||
|
||||
protected function hasConflict(string $userId, Carbon $start, Carbon $end): bool
|
||||
{
|
||||
// 1. Eintägige Termine: einfacher Zeitüberlapp (ganztägige ignorieren)
|
||||
// 1. Eintägige Termine: einfacher Zeitüberlapp
|
||||
$singleDayConflict = Event::where('user_id', $userId)
|
||||
->where('is_all_day', false)
|
||||
->whereRaw('DATE(starts_at) = DATE(ends_at)')
|
||||
->where(function ($q) use ($start, $end) {
|
||||
$q->where('starts_at', '<', $end)
|
||||
|
|
@ -249,16 +244,15 @@ class EventPlannerService
|
|||
|
||||
protected function hasMultiDayConflict(string $userId, Carbon $start, Carbon $end): bool
|
||||
{
|
||||
// Alle echten Mehrtagesevents deren Datumsbereich den neuen Termin überschneidet (ganztägige ignorieren)
|
||||
// Alle echten Mehrtagesevents deren Datumsbereich den neuen Termin überschneidet
|
||||
$candidates = Event::where('user_id', $userId)
|
||||
->where('is_all_day', false)
|
||||
->whereRaw('DATE(starts_at) < DATE(ends_at)')
|
||||
->whereRaw('DATE(starts_at) <= ?', [$end->toDateString()])
|
||||
->whereRaw('DATE(ends_at) >= ?', [$start->toDateString()])
|
||||
->get();
|
||||
|
||||
foreach ($candidates as $event) {
|
||||
// Reine ganztägige Termine ohne spezifische Zeiten überspringen (Fallback)
|
||||
// Reine ganztägige Termine ohne spezifische Zeiten überspringen
|
||||
if ($event->starts_at->format('H:i') === '00:00'
|
||||
&& in_array($event->ends_at->format('H:i'), ['23:59', '00:00'])) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -42,14 +42,10 @@ class PushService
|
|||
])->values()->toArray();
|
||||
|
||||
try {
|
||||
$expoToken = config('services.expo.token');
|
||||
|
||||
$response = Http::asJson()
|
||||
->withHeaders(array_filter([
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => $expoToken ? 'Bearer ' . $expoToken : null,
|
||||
]))
|
||||
->post(self::EXPO_PUSH_URL, $messages);
|
||||
'Content-Type' => 'application/json',
|
||||
])->post(self::EXPO_PUSH_URL, $messages);
|
||||
|
||||
$receipts = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ return [
|
|||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'api_url' => env('APP_API_URL', 'http://localhost'),
|
||||
'api_url' => env('APP_CONNECT_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'paths' => ['api/*', 'v1/*'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => [
|
||||
env('APP_URL', 'http://localhost'),
|
||||
env('APP_CONNECT_URL', 'http://localhost'),
|
||||
// Lokal:
|
||||
'http://app.aziros.local',
|
||||
'http://api.aziros.local',
|
||||
// Staging:
|
||||
'https://app.staging.aziros.com',
|
||||
// Production:
|
||||
'https://app.aziros.com',
|
||||
'https://www.aziros.com',
|
||||
],
|
||||
|
||||
'allowed_origins_patterns' => [
|
||||
'#^https?://.*\.aziros\.(com|local)$#',
|
||||
],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
'supports_credentials' => true,
|
||||
];
|
||||
|
|
@ -55,8 +55,8 @@ return [
|
|||
'host' => env('MAIL_HOST'),
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_SYSTEM_USERNAME', env('MAIL_USERNAME')),
|
||||
'password' => env('MAIL_SYSTEM_PASSWORD', env('MAIL_PASSWORD')),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'from' => [
|
||||
'address' => env('MAIL_SYSTEM_USERNAME', env('MAIL_USERNAME')),
|
||||
'name' => 'Aziros',
|
||||
|
|
@ -68,8 +68,8 @@ return [
|
|||
'host' => env('MAIL_HOST'),
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_REMINDER_USERNAME', env('MAIL_USERNAME')),
|
||||
'password' => env('MAIL_REMINDER_PASSWORD', env('MAIL_PASSWORD')),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'from' => [
|
||||
'address' => env('MAIL_REMINDER_USERNAME', env('MAIL_USERNAME')),
|
||||
'name' => 'Aziros Reminder',
|
||||
|
|
@ -81,8 +81,8 @@ return [
|
|||
'host' => env('MAIL_HOST'),
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_ARIA_USERNAME', env('MAIL_USERNAME')),
|
||||
'password' => env('MAIL_ARIA_PASSWORD', env('MAIL_PASSWORD')),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'from' => [
|
||||
'address' => env('MAIL_ARIA_USERNAME', env('MAIL_USERNAME')),
|
||||
'name' => 'Aria',
|
||||
|
|
@ -94,8 +94,8 @@ return [
|
|||
'host' => env('MAIL_HOST'),
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_HELLO_USERNAME', env('MAIL_USERNAME')),
|
||||
'password' => env('MAIL_HELLO_PASSWORD', env('MAIL_PASSWORD')),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'from' => [
|
||||
'address' => env('MAIL_HELLO_USERNAME', env('MAIL_USERNAME')),
|
||||
'name' => 'Aziros',
|
||||
|
|
|
|||
|
|
@ -42,10 +42,6 @@ return [
|
|||
'currency' => env('STRIPE_CURRENCY', 'eur'),
|
||||
],
|
||||
|
||||
'expo' => [
|
||||
'token' => env('EXPO_TOKEN'),
|
||||
],
|
||||
|
||||
'openai' => [
|
||||
'key' => env('OPENAI_API_KEY'),
|
||||
'model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('events', function (Blueprint $table) {
|
||||
$table->timestamp('reminder_at')->nullable()->after('reminders');
|
||||
$table->boolean('reminder_sent')->default(false)->after('reminder_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('events', function (Blueprint $table) {
|
||||
$table->dropColumn(['reminder_at', 'reminder_sent']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('withdrawal_waivers', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUuid('plan_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('plan_name');
|
||||
$table->string('billing', 20);
|
||||
$table->unsignedInteger('amount_cents');
|
||||
$table->string('checkout_type', 20);
|
||||
$table->boolean('right_acknowledged')->default(false);
|
||||
$table->boolean('waiver_confirmed')->default(false);
|
||||
$table->timestamp('confirmed_at');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->string('pdf_path')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('withdrawal_waivers');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Translation;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$keys = [
|
||||
'events.edit' => ['de' => 'Termin bearbeiten', 'en' => 'Edit event'],
|
||||
'events.new' => ['de' => 'Neuer Termin', 'en' => 'New event'],
|
||||
];
|
||||
|
||||
foreach ($keys as $key => $locales) {
|
||||
foreach ($locales as $locale => $value) {
|
||||
Translation::updateOrCreate(
|
||||
['key' => $key, 'locale' => $locale],
|
||||
['value' => $value]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
|
|
@ -45,7 +45,6 @@ class FeatureSeeder extends Seeder
|
|||
['key' => 'ai_agent', 'label' => 'KI-Assistent', 'icon' => 'heroicon-o-sparkles', 'group' => $productivity, 'sort' => 3],
|
||||
['key' => 'calendar_sync', 'label' => 'Kalender-Synchronisierung', 'icon' => 'heroicon-o-arrow-path', 'group' => $integration, 'sort' => 1],
|
||||
['key' => 'automations', 'label' => 'Automationen', 'icon' => 'heroicon-o-bolt', 'group' => $integration, 'sort' => 2],
|
||||
['key' => 'api_access', 'label' => 'API-Zugang', 'icon' => 'heroicon-o-code-bracket', 'group' => $integration, 'sort' => 3],
|
||||
['key' => 'speed', 'label' => 'GPT-4o Modell (schneller & präziser)', 'icon' => 'heroicon-o-bolt', 'group' => $performance, 'sort' => 1],
|
||||
];
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ class FeatureSeeder extends Seeder
|
|||
$freeFeatures = ['calendar', 'reminders', 'tasks', 'notes', 'contacts', 'ai_agent'];
|
||||
|
||||
// Features die nur Pro bekommt
|
||||
$proFeatures = ['calendar_sync', 'automations', 'api_access', 'speed'];
|
||||
$proFeatures = ['calendar_sync', 'automations', 'speed'];
|
||||
|
||||
// Bestehende Verknüpfungen leeren
|
||||
DB::table('feature_plan')->delete();
|
||||
|
|
|
|||
|
|
@ -244,15 +244,10 @@ class TranslationSeeder extends Seeder
|
|||
'calendar.no_events' => ['de' => 'Keine Termine', 'en' => 'No events'],
|
||||
|
||||
// ── Event Form ──────────────────────────
|
||||
'events.edit' => ['de' => 'Termin bearbeiten', 'en' => 'Edit event'],
|
||||
'events.new' => ['de' => 'Neuer Termin', 'en' => 'New event'],
|
||||
'events.title' => ['de' => 'Titel', 'en' => 'Title'],
|
||||
'events.title_placeholder' => ['de' => 'Titel hinzufügen', 'en' => 'Add title'],
|
||||
'events.datetime' => ['de' => 'Datum & Zeit', 'en' => 'Date & Time'],
|
||||
'events.date' => ['de' => 'Datum', 'en' => 'Date'],
|
||||
'events.select_date' => ['de' => 'Datum wählen', 'en' => 'Select date'],
|
||||
'events.select_time' => ['de' => 'Uhrzeit wählen', 'en' => 'Select time'],
|
||||
'events.reminder_specific_short' => ['de' => 'Eigenes Datum', 'en' => 'Custom date'],
|
||||
'events.multiday' => ['de' => 'Mehrtägig', 'en' => 'Multi-day'],
|
||||
'events.custom_days' => ['de' => 'Einzelne Tage anpassen', 'en' => 'Customize individual days'],
|
||||
'events.custom' => ['de' => 'Custom', 'en' => 'Custom'],
|
||||
|
|
@ -290,12 +285,10 @@ class TranslationSeeder extends Seeder
|
|||
'tasks.priority_medium' => ['de' => 'Mittel', 'en' => 'Medium'],
|
||||
'tasks.priority_high' => ['de' => 'Hoch', 'en' => 'High'],
|
||||
'tasks.due_at' => ['de' => 'Fällig am', 'en' => 'Due on'],
|
||||
'tasks.due_optional' => ['de' => 'Datum wählen (optional)', 'en' => 'Select date (optional)'],
|
||||
'tasks.no_tasks' => ['de' => 'Keine Aufgaben gefunden', 'en' => 'No tasks found'],
|
||||
'tasks.create_first' => ['de' => 'Erste Aufgabe erstellen', 'en' => 'Create first task'],
|
||||
'tasks.confirm_delete' => ['de' => 'Aufgabe löschen?', 'en' => 'Delete task?'],
|
||||
'tasks.to_in_progress' => ['de' => '→ In Bearbeitung', 'en' => '→ In progress'],
|
||||
'tasks.reminder_custom' => ['de' => 'Eigene Zeit', 'en' => 'Custom time'],
|
||||
|
||||
// ── Notes ───────────────────────────────
|
||||
'notes.color.yellow' => ['de' => 'Gelb', 'en' => 'Yellow'],
|
||||
|
|
@ -760,22 +753,6 @@ class TranslationSeeder extends Seeder
|
|||
'checkout.redirect_stripe' => ['de' => 'Du wirst sicher zu Stripe weitergeleitet.', 'en' => 'You will be securely redirected to Stripe.'],
|
||||
'checkout.no_checkout' => ['de' => 'Kein Checkout nötig – Stripe verarbeitet die Änderung direkt.', 'en' => 'No checkout needed – Stripe processes the change directly.'],
|
||||
'checkout.cancel_effective' => ['de' => 'Kündigung wird sofort wirksam.', 'en' => 'Cancellation takes effect immediately.'],
|
||||
'checkout.waiver_info' => [
|
||||
'de' => 'Hinweis zum Widerrufsrecht: Du hast das Recht, diesen Vertrag innerhalb von 14 Tagen ohne Angabe von Gründen zu widerrufen. Da wir die Dienstleistung sofort bereitstellen, erlischt dein Widerrufsrecht mit Beginn der Leistungserbringung – sofern du ausdrücklich zustimmst.',
|
||||
'en' => 'Right of withdrawal notice: You have the right to withdraw from this contract within 14 days without giving any reason. Since we provide the service immediately, your right of withdrawal expires upon commencement of the service – provided you expressly agree.',
|
||||
],
|
||||
'checkout.waiver_right_acknowledged' => [
|
||||
'de' => 'Ich bestätige, dass ich über mein 14-tägiges Widerrufsrecht informiert wurde.',
|
||||
'en' => 'I confirm that I have been informed about my 14-day right of withdrawal.',
|
||||
],
|
||||
'checkout.waiver_confirmed' => [
|
||||
'de' => 'Ich verlange ausdrücklich die sofortige Ausführung des Vertrags und bestätige, dass ich mit Beginn der Leistungserbringung mein Widerrufsrecht verliere.',
|
||||
'en' => 'I expressly request the immediate execution of the contract and acknowledge that I will lose my right of withdrawal upon commencement of the service.',
|
||||
],
|
||||
'checkout.waiver_required' => [
|
||||
'de' => 'Bitte bestätige beide Checkboxen zum Widerrufsrecht.',
|
||||
'en' => 'Please confirm both checkboxes regarding the right of withdrawal.',
|
||||
],
|
||||
|
||||
// ── Checkout Success ─────────────────────
|
||||
'checkout.success.processing' => ['de' => 'Zahlung wird verarbeitet…', 'en' => 'Processing payment…'],
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));export{s as n,l as r,o as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,102 +0,0 @@
|
|||
{
|
||||
"_axios-CH1o5aW5.js": {
|
||||
"file": "assets/axios-CH1o5aW5.js",
|
||||
"name": "axios",
|
||||
"imports": [
|
||||
"_rolldown-runtime-XQCOJYun.js"
|
||||
]
|
||||
},
|
||||
"_rolldown-runtime-XQCOJYun.js": {
|
||||
"file": "assets/rolldown-runtime-XQCOJYun.js",
|
||||
"name": "rolldown-runtime"
|
||||
},
|
||||
"_vendor-BJQRk5yT.js": {
|
||||
"file": "assets/vendor-BJQRk5yT.js",
|
||||
"name": "vendor",
|
||||
"imports": [
|
||||
"_rolldown-runtime-XQCOJYun.js"
|
||||
]
|
||||
},
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-C9yp60BI.css",
|
||||
"name": "app",
|
||||
"names": [
|
||||
"app.css"
|
||||
],
|
||||
"src": "resources/css/app.css",
|
||||
"isEntry": true,
|
||||
"assets": [
|
||||
"assets/bai-jamjuree-200-BNt7RBly.woff2",
|
||||
"assets/bai-jamjuree-200italic-BKLgs9tE.woff2",
|
||||
"assets/bai-jamjuree-300-tJsyrsLz.woff2",
|
||||
"assets/bai-jamjuree-300italic-CEl8Yjrk.woff2",
|
||||
"assets/bai-jamjuree-regular-DkJufkaw.woff2",
|
||||
"assets/bai-jamjuree-italic-CTCl9qLZ.woff2",
|
||||
"assets/bai-jamjuree-500-B5fxNtsw.woff2",
|
||||
"assets/bai-jamjuree-500italic-CXrosT7a.woff2",
|
||||
"assets/bai-jamjuree-600-D6So4yha.woff2",
|
||||
"assets/bai-jamjuree-600italic-6wcHKQVd.woff2",
|
||||
"assets/bai-jamjuree-700-D9sAOCG2.woff2",
|
||||
"assets/bai-jamjuree-700italic-LW2Ny60n.woff2"
|
||||
]
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-200.woff2": {
|
||||
"file": "assets/bai-jamjuree-200-BNt7RBly.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-200.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-200italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-200italic-BKLgs9tE.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-200italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-300.woff2": {
|
||||
"file": "assets/bai-jamjuree-300-tJsyrsLz.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-300.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-300italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-300italic-CEl8Yjrk.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-300italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-500.woff2": {
|
||||
"file": "assets/bai-jamjuree-500-B5fxNtsw.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-500.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-500italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-500italic-CXrosT7a.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-500italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-600.woff2": {
|
||||
"file": "assets/bai-jamjuree-600-D6So4yha.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-600.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-600italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-600italic-6wcHKQVd.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-600italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-700.woff2": {
|
||||
"file": "assets/bai-jamjuree-700-D9sAOCG2.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-700.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-700italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-700italic-LW2Ny60n.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-700italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-italic.woff2": {
|
||||
"file": "assets/bai-jamjuree-italic-CTCl9qLZ.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-italic.woff2"
|
||||
},
|
||||
"resources/fonts/BaiJamjuree/bai-jamjuree-regular.woff2": {
|
||||
"file": "assets/bai-jamjuree-regular-DkJufkaw.woff2",
|
||||
"src": "resources/fonts/BaiJamjuree/bai-jamjuree-regular.woff2"
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-BjEYLZEv.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_rolldown-runtime-XQCOJYun.js",
|
||||
"_axios-CH1o5aW5.js",
|
||||
"_vendor-BJQRk5yT.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 119.1 119.1" style="color:#ffffff">
|
||||
<!-- Generator: Adobe Illustrator 30.0.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 123) -->
|
||||
<defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { color: #ffffff; }
|
||||
}
|
||||
|
||||
.st0 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
letter-spacing: .8em;
|
||||
}
|
||||
|
||||
.st1, .st2 {
|
||||
font-family: Geometos, Geometos;
|
||||
font-size: 12px;
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
letter-spacing: .5em;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #8f5a4d;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<text class="st1" transform="translate(185.6 -114.1)"><tspan x="0" y="0">AI PRODUCTIVITY ASSISTANT</tspan></text>
|
||||
<path class="st4" d="M539.8-163.9v-1.4h0c0,1.5,0,2.9.2,4.2,0-.9-.1-1.8-.1-2.7ZM576.6-137.1h-1"/>
|
||||
<path class="st4" d="M329.5-210.5v8.4c0,.8-.3,1.6-.8,2.2l-.8,1-14.5,16.8c-.6.7-1.6,1.2-2.6,1.2h-10.6c-2.9,0-4.5-3.4-2.6-5.6l10.7-12.3h-37.4c-1.9,0-3.4-1.5-3.4-3.4v-8.2c0-1.9,1.5-3.4,3.4-3.4h55.2c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<rect class="st4" x="343.7" y="-213.9" width="15" height="26" rx="-80.9" ry="-80.9"/>
|
||||
<rect class="st4" x="343.7" y="-183" width="15" height="45.1" rx="-46.4" ry="-46.4"/>
|
||||
<line class="st0" x1="358.7" y1="-137.9" x2="343.7" y2="-137.9"/>
|
||||
<line class="st0" x1="392.6" y1="-137.9" x2="377.6" y2="-137.9"/>
|
||||
<path class="st4" d="M439.2-137.9c2.7,0,4.3-3,2.9-5.3l-13.3-20.9-1.7-2.8c5.6-2.3,10.1-6.8,12.4-12.3,1.1-2.7,1.7-5.7,1.7-8.8v-3.2c0-2.8-.5-5.5-1.4-8-3.2-8.7-11.5-14.8-21.2-14.8h-37.5c-1.9,0-3.4,1.5-3.4,3.4v7.9c0,1.9,1.5,3.4,3.4,3.4h36.4c.3,0,.6,0,.9,0,4.3.5,7.6,4.1,7.6,8.5v2.9c0,4.7-3.8,8.6-8.5,8.6h-36.4c-1.9,0-3.4,1.5-3.4,3.4v34.5c0,1.9,1.5,3.4,3.4,3.4h8.2c1.9,0,3.4-1.5,3.4-3.4v-23.9h17.7l.7,1.1,15.5,24.6c.6,1,1.7,1.6,2.9,1.6h9.9Z"/>
|
||||
<path class="st4" d="M329.5-148.5v7.2c0,1.9-1.5,3.4-3.4,3.4h-55.2c-1.9,0-3.4-1.5-3.4-3.4v-7.7c0-.8.3-1.6.8-2.2l.6-.7,14.9-17.1c.6-.7,1.6-1.2,2.6-1.2h10.6c2.9,0,4.5,3.4,2.6,5.6l-11.1,12.6h37.6c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<path class="st4" d="M218.2-213.9h10.6c1.4,0,2.6.8,3.1,2.1l29.2,69.3c.9,2.2-.7,4.7-3.1,4.7h-68.9c-2.4,0-4.1-2.5-3.1-4.7l3-7.1v-.2c.6-1.3,1.8-2.1,3.2-2.1h48.7l-17.4-41.2-13.9,33.1c-.5,1.3-1.8,2.1-3.1,2.1h-8.9c-2.4,0-4.1-2.5-3.1-4.7l20.7-49.2c.5-1.3,1.8-2.1,3.1-2.1Z"/>
|
||||
<path class="st4" d="M487.5-210.6v8.3c0,1.5-1,2.9-2.5,3.2-10.6,2.7-18.4,12-18.4,23.2s7.8,20.5,18.4,23.2c1.5.4,2.5,1.7,2.5,3.2v8.2c0,2.1-2,3.7-4.1,3.3-18.2-3.5-31.9-19.2-31.9-38s13.7-34.5,31.9-38,4.1,1.2,4.1,3.3Z"/>
|
||||
<path class="st4" d="M530.8-175.9c0,18.9-13.9,34.7-32.3,38-2.1.4-4-1.2-4-3.3v-8.2c0-1.5,1-2.9,2.5-3.3,10.8-2.5,18.7-12,18.7-23.3s-8-20.7-18.7-23.3-2.5-1.7-2.5-3.3v-8.2c0-2.1,1.9-3.7,4-3.3,18.4,3.3,32.3,19.1,32.3,38Z"/>
|
||||
<line class="st0" x1="358.7" y1="-137.9" x2="343.7" y2="-137.9"/>
|
||||
<line class="st0" x1="392.6" y1="-137.9" x2="377.6" y2="-137.9"/>
|
||||
<path class="st4" d="M594.9-206.1c2.3,2.3,4.2,5,5.5,8,0,0,0,.1,0,.2.9,2.2,1.5,4.6,1.8,7.4s-.8,2.1-2,2.1h-10.6c-1,0-1.9-.8-2-1.8-.3-3.3-1.3-4.6-3.2-6.5s-5.1-3.2-7.4-3.6-1.6-1-1.6-1.9v-10.1c0-.9.8-1.7,1.7-1.7h0c7.4,0,12.6,2.9,17.5,7.7"/>
|
||||
<path class="st4" d="M568.6-149.6v9.8c0,1-.9,1.9-1.9,1.9h0c-8.5,0-16-3.9-20.9-9.9,0,0,0,0,0,0-2.4-3-4.2-6.5-5.1-10.3,0-.3-.1-.5-.2-.8,0-.4-.2-.9-.2-1.4,0-.4-.1-.7-.1-1.1,0,0,0-.1,0-.2,0-.9-.1-1.4-.2-2v-.2c0-1.1.9-1.9,2-1.9h10.1c1,0,1.9.8,2,1.8.5,5.1,2.8,8.5,6.6,10.3,1.9.9,4.4,1.6,6.6,2s1.6,1,1.6,1.9Z"/>
|
||||
<path class="st4" d="M603-160.3c0,1.3,0,2.5-.2,3.6-.9,5-3.4,9.4-7,12.7-4.1,3.8-9.7,6.1-15.8,6.1h-2.5c-1.1,0-2-.9-2-2v-9.3c0-1,.7-1.8,1.7-1.9s2-.4,2.8-.6c1.8-.6,3.3-1.3,4.6-2.2s2.2-1.9,2.8-3.1c.6-1.2.9-2.3.9-3.5s-.5-2.7-1.4-3.8c-.9-1-2.2-1.9-3.7-2.6s-3.4-1.2-5.5-1.7c-2.1-.5-4.3-.9-6.6-1.3-.6,0-1.2-.2-1.8-.3-1.1-.2-2.2-.4-3.5-.6-2-.4-4.1-.9-6.2-1.5-2.1-.6-4.3-1.5-6.3-2.5-2.1-1-4-2.3-5.6-3.9-1-.9-1.8-1.9-2.6-3.1-.5-.8-1-1.7-1.4-2.6-1-2.2-1.5-4.7-1.5-7.7s.4-5.5,1.2-7.7c0,0,0-.2.1-.3.6-1.3,1.3-2.6,2.1-3.8,0,0,0,0,0,0,0,0,0,0,0,0,4.2-6.1,11-10.2,19-10.2h2c1.1,0,1.9.9,1.9,2v9.6c0,.9-.7,1.8-1.6,1.9s-.5,0-.8.1c-1.6.3-3.1.8-4.5,1.5s-2.6,1.5-3.5,2.6c-1,1.1-1.4,2.3-1.4,3.9,0,1.4.7,2.8,1.8,3.7,1.1,1,2.5,1.8,4.2,2.4,1.7.7,3.6,1.2,5.7,1.7,2.1.5,4.1.9,6,1.4,1.5.3,3.2.7,5.2,1,.9.1,1.7.3,2.6.5,1.1.3,2.3.6,3.5.9,2.1.6,4.2,1.4,6.3,2.4,2.1,1,3.9,2.3,5.6,3.9,1.3,1.2,2.4,2.7,3.3,4.4.2.4.5.9.7,1.4,1,2.3,1.5,5.1,1.5,8.4Z"/>
|
||||
<text class="st2" transform="translate(22.7 104.6)"><tspan x="0" y="0">AZIROS</tspan></text>
|
||||
<line class="st0" x1="195.8" y1="89.9" x2="180.8" y2="89.9"/>
|
||||
<line class="st0" x1="229.7" y1="89.9" x2="214.7" y2="89.9"/>
|
||||
<path class="st4" d="M55.3,13.9h10.6c1.4,0,2.6.8,3.1,2.1l29.2,69.3c.9,2.2-.7,4.7-3.1,4.7H26.2c-2.4,0-4.1-2.5-3.1-4.7l3-7.1v-.2c.6-1.3,1.8-2.1,3.2-2.1h48.7l-17.4-41.2-13.9,33.1c-.5,1.3-1.8,2.1-3.1,2.1h-8.9c-2.4,0-4.1-2.5-3.1-4.7L52.1,16c.5-1.3,1.8-2.1,3.1-2.1Z"/>
|
||||
<line class="st0" x1="195.8" y1="89.9" x2="180.8" y2="89.9"/>
|
||||
<line class="st0" x1="229.7" y1="89.9" x2="214.7" y2="89.9"/>
|
||||
<rect class="st3" x="-158.8" y="86.6" width="52.6" height="96.6"/>
|
||||
<rect class="st3" x="-101" y="85.4" width="52.6" height="96.6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
|
@ -1,47 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 119.1 119.1" style="color:#1d1d1b">
|
||||
<!-- Generator: Adobe Illustrator 30.0.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 123) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 { fill: none; }
|
||||
.st1 { letter-spacing: .8em; }
|
||||
.st1, .st2 {
|
||||
font-family: Geometos, Geometos;
|
||||
font-size: 12px;
|
||||
fill: currentColor;
|
||||
}
|
||||
.st2 { letter-spacing: .5em; }
|
||||
.st3 { fill: #8f5a4d; opacity: .6; }
|
||||
.st4 { fill: currentColor; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
svg { color: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<text class="st1" transform="translate(185.6 -114.1)"><tspan x="0" y="0">AI PRODUCTIVITY ASSISTANT</tspan></text>
|
||||
<path class="st4" d="M539.8-163.9v-1.4h0c0,1.5,0,2.9.2,4.2,0-.9-.1-1.8-.1-2.7ZM576.6-137.1h-1"/>
|
||||
<path class="st4" d="M329.5-210.5v8.4c0,.8-.3,1.6-.8,2.2l-.8,1-14.5,16.8c-.6.7-1.6,1.2-2.6,1.2h-10.6c-2.9,0-4.5-3.4-2.6-5.6l10.7-12.3h-37.4c-1.9,0-3.4-1.5-3.4-3.4v-8.2c0-1.9,1.5-3.4,3.4-3.4h55.2c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<rect class="st4" x="343.7" y="-213.9" width="15" height="26" rx="-80.9" ry="-80.9"/>
|
||||
<rect class="st4" x="343.7" y="-183" width="15" height="45.1" rx="-46.4" ry="-46.4"/>
|
||||
<line class="st0" x1="358.7" y1="-137.9" x2="343.7" y2="-137.9"/>
|
||||
<line class="st0" x1="392.6" y1="-137.9" x2="377.6" y2="-137.9"/>
|
||||
<path class="st4" d="M439.2-137.9c2.7,0,4.3-3,2.9-5.3l-13.3-20.9-1.7-2.8c5.6-2.3,10.1-6.8,12.4-12.3,1.1-2.7,1.7-5.7,1.7-8.8v-3.2c0-2.8-.5-5.5-1.4-8-3.2-8.7-11.5-14.8-21.2-14.8h-37.5c-1.9,0-3.4,1.5-3.4,3.4v7.9c0,1.9,1.5,3.4,3.4,3.4h36.4c.3,0,.6,0,.9,0,4.3.5,7.6,4.1,7.6,8.5v2.9c0,4.7-3.8,8.6-8.5,8.6h-36.4c-1.9,0-3.4,1.5-3.4,3.4v34.5c0,1.9,1.5,3.4,3.4,3.4h8.2c1.9,0,3.4-1.5,3.4-3.4v-23.9h17.7l.7,1.1,15.5,24.6c.6,1,1.7,1.6,2.9,1.6h9.9Z"/>
|
||||
<path class="st4" d="M329.5-148.5v7.2c0,1.9-1.5,3.4-3.4,3.4h-55.2c-1.9,0-3.4-1.5-3.4-3.4v-7.7c0-.8.3-1.6.8-2.2l.6-.7,14.9-17.1c.6-.7,1.6-1.2,2.6-1.2h10.6c2.9,0,4.5,3.4,2.6,5.6l-11.1,12.6h37.6c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<path class="st4" d="M218.2-213.9h10.6c1.4,0,2.6.8,3.1,2.1l29.2,69.3c.9,2.2-.7,4.7-3.1,4.7h-68.9c-2.4,0-4.1-2.5-3.1-4.7l3-7.1v-.2c.6-1.3,1.8-2.1,3.2-2.1h48.7l-17.4-41.2-13.9,33.1c-.5,1.3-1.8,2.1-3.1,2.1h-8.9c-2.4,0-4.1-2.5-3.1-4.7l20.7-49.2c.5-1.3,1.8-2.1,3.1-2.1Z"/>
|
||||
<path class="st4" d="M487.5-210.6v8.3c0,1.5-1,2.9-2.5,3.2-10.6,2.7-18.4,12-18.4,23.2s7.8,20.5,18.4,23.2c1.5.4,2.5,1.7,2.5,3.2v8.2c0,2.1-2,3.7-4.1,3.3-18.2-3.5-31.9-19.2-31.9-38s13.7-34.5,31.9-38,4.1,1.2,4.1,3.3Z"/>
|
||||
<path class="st4" d="M530.8-175.9c0,18.9-13.9,34.7-32.3,38-2.1.4-4-1.2-4-3.3v-8.2c0-1.5,1-2.9,2.5-3.3,10.8-2.5,18.7-12,18.7-23.3s-8-20.7-18.7-23.3-2.5-1.7-2.5-3.3v-8.2c0-2.1,1.9-3.7,4-3.3,18.4,3.3,32.3,19.1,32.3,38Z"/>
|
||||
<line class="st0" x1="358.7" y1="-137.9" x2="343.7" y2="-137.9"/>
|
||||
<line class="st0" x1="392.6" y1="-137.9" x2="377.6" y2="-137.9"/>
|
||||
<path class="st4" d="M594.9-206.1c2.3,2.3,4.2,5,5.5,8,0,0,0,.1,0,.2.9,2.2,1.5,4.6,1.8,7.4s-.8,2.1-2,2.1h-10.6c-1,0-1.9-.8-2-1.8-.3-3.3-1.3-4.6-3.2-6.5s-5.1-3.2-7.4-3.6-1.6-1-1.6-1.9v-10.1c0-.9.8-1.7,1.7-1.7h0c7.4,0,12.6,2.9,17.5,7.7"/>
|
||||
<path class="st4" d="M568.6-149.6v9.8c0,1-.9,1.9-1.9,1.9h0c-8.5,0-16-3.9-20.9-9.9,0,0,0,0,0,0-2.4-3-4.2-6.5-5.1-10.3,0-.3-.1-.5-.2-.8,0-.4-.2-.9-.2-1.4,0-.4-.1-.7-.1-1.1,0,0,0-.1,0-.2,0-.9-.1-1.4-.2-2v-.2c0-1.1.9-1.9,2-1.9h10.1c1,0,1.9.8,2,1.8.5,5.1,2.8,8.5,6.6,10.3,1.9.9,4.4,1.6,6.6,2s1.6,1,1.6,1.9Z"/>
|
||||
<path class="st4" d="M603-160.3c0,1.3,0,2.5-.2,3.6-.9,5-3.4,9.4-7,12.7-4.1,3.8-9.7,6.1-15.8,6.1h-2.5c-1.1,0-2-.9-2-2v-9.3c0-1,.7-1.8,1.7-1.9s2-.4,2.8-.6c1.8-.6,3.3-1.3,4.6-2.2s2.2-1.9,2.8-3.1c.6-1.2.9-2.3.9-3.5s-.5-2.7-1.4-3.8c-.9-1-2.2-1.9-3.7-2.6s-3.4-1.2-5.5-1.7c-2.1-.5-4.3-.9-6.6-1.3-.6,0-1.2-.2-1.8-.3-1.1-.2-2.2-.4-3.5-.6-2-.4-4.1-.9-6.2-1.5-2.1-.6-4.3-1.5-6.3-2.5-2.1-1-4-2.3-5.6-3.9-1-.9-1.8-1.9-2.6-3.1-.5-.8-1-1.7-1.4-2.6-1-2.2-1.5-4.7-1.5-7.7s.4-5.5,1.2-7.7c0,0,0-.2.1-.3.6-1.3,1.3-2.6,2.1-3.8,0,0,0,0,0,0,0,0,0,0,0,0,4.2-6.1,11-10.2,19-10.2h2c1.1,0,1.9.9,1.9,2v9.6c0,.9-.7,1.8-1.6,1.9s-.5,0-.8.1c-1.6.3-3.1.8-4.5,1.5s-2.6,1.5-3.5,2.6c-1,1.1-1.4,2.3-1.4,3.9,0,1.4.7,2.8,1.8,3.7,1.1,1,2.5,1.8,4.2,2.4,1.7.7,3.6,1.2,5.7,1.7,2.1.5,4.1.9,6,1.4,1.5.3,3.2.7,5.2,1,.9.1,1.7.3,2.6.5,1.1.3,2.3.6,3.5.9,2.1.6,4.2,1.4,6.3,2.4,2.1,1,3.9,2.3,5.6,3.9,1.3,1.2,2.4,2.7,3.3,4.4.2.4.5.9.7,1.4,1,2.3,1.5,5.1,1.5,8.4Z"/>
|
||||
<text class="st2" transform="translate(22.7 104.6)"><tspan x="0" y="0">AZIROS</tspan></text>
|
||||
<line class="st0" x1="195.8" y1="89.9" x2="180.8" y2="89.9"/>
|
||||
<line class="st0" x1="229.7" y1="89.9" x2="214.7" y2="89.9"/>
|
||||
<path class="st4" d="M55.3,13.9h10.6c1.4,0,2.6.8,3.1,2.1l29.2,69.3c.9,2.2-.7,4.7-3.1,4.7H26.2c-2.4,0-4.1-2.5-3.1-4.7l3-7.1v-.2c.6-1.3,1.8-2.1,3.2-2.1h48.7l-17.4-41.2-13.9,33.1c-.5,1.3-1.8,2.1-3.1,2.1h-8.9c-2.4,0-4.1-2.5-3.1-4.7L52.1,16c.5-1.3,1.8-2.1,3.1-2.1Z"/>
|
||||
<line class="st0" x1="195.8" y1="89.9" x2="180.8" y2="89.9"/>
|
||||
<line class="st0" x1="229.7" y1="89.9" x2="214.7" y2="89.9"/>
|
||||
<rect class="st3" x="-158.8" y="86.6" width="52.6" height="96.6"/>
|
||||
<rect class="st3" x="-101" y="85.4" width="52.6" height="96.6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 491.2 162.3">
|
||||
<!-- Generator: Adobe Illustrator 30.0.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 123) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
font-family: Geometos, Geometos;
|
||||
font-size: 12px;
|
||||
letter-spacing: .8em;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #1d1d1b;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<text class="st0" transform="translate(33.8 129.8)"><tspan x="0" y="0">AI PRODUCTIVITY ASSISTANT</tspan></text>
|
||||
<path class="st1" d="M388,80v-1.4h0c0,1.5,0,2.9.2,4.2,0-.9-.1-1.8-.1-2.7ZM424.8,106.8h-1"/>
|
||||
<path class="st1" d="M177.7,33.3v8.4c0,.8-.3,1.6-.8,2.2l-.8,1-14.5,16.8c-.6.7-1.6,1.2-2.6,1.2h-10.6c-2.9,0-4.5-3.4-2.6-5.6l10.7-12.3h-37.4c-1.9,0-3.4-1.5-3.4-3.4v-8.2c0-1.9,1.5-3.4,3.4-3.4h55.2c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<rect class="st1" x="191.9" y="30" width="15" height="26" rx="3.4" ry="3.4"/>
|
||||
<rect class="st1" x="191.9" y="60.9" width="15" height="45.1" rx="3.4" ry="3.4"/>
|
||||
<path class="st1" d="M287.4,106c2.7,0,4.3-3,2.9-5.3l-13.3-20.9-1.7-2.8c5.6-2.3,10.1-6.8,12.4-12.3,1.1-2.7,1.7-5.7,1.7-8.8v-3.2c0-2.8-.5-5.5-1.4-8-3.2-8.7-11.5-14.8-21.2-14.8h-37.5c-1.9,0-3.4,1.5-3.4,3.4v7.9c0,1.9,1.5,3.4,3.4,3.4h36.4c.3,0,.6,0,.9,0,4.3.5,7.6,4.1,7.6,8.5v2.9c0,4.7-3.8,8.6-8.5,8.6h-36.4c-1.9,0-3.4,1.5-3.4,3.4v34.5c0,1.9,1.5,3.4,3.4,3.4h8.2c1.9,0,3.4-1.5,3.4-3.4v-23.9h17.7l.7,1.1,15.5,24.6c.6,1,1.7,1.6,2.9,1.6h9.9Z"/>
|
||||
<path class="st1" d="M177.7,95.4v7.2c0,1.9-1.5,3.4-3.4,3.4h-55.2c-1.9,0-3.4-1.5-3.4-3.4v-7.7c0-.8.3-1.6.8-2.2l.6-.7,14.9-17.1c.6-.7,1.6-1.2,2.6-1.2h10.6c2.9,0,4.5,3.4,2.6,5.6l-11.1,12.6h37.6c1.9,0,3.4,1.5,3.4,3.4Z"/>
|
||||
<path class="st1" d="M66.4,29.9h10.6c1.4,0,2.6.8,3.1,2.1l29.2,69.3c.9,2.2-.7,4.7-3.1,4.7H37.3c-2.4,0-4.1-2.5-3.1-4.7l3-7.1v-.2c.6-1.3,1.8-2.1,3.2-2.1h48.7l-17.4-41.2-13.9,33.1c-.5,1.3-1.8,2.1-3.1,2.1h-8.9c-2.4,0-4.1-2.5-3.1-4.7l20.7-49.2c.5-1.3,1.8-2.1,3.1-2.1Z"/>
|
||||
<path class="st1" d="M335.7,33.3v8.3c0,1.5-1,2.9-2.5,3.2-10.6,2.7-18.4,12-18.4,23.2s7.8,20.5,18.4,23.2c1.5.4,2.5,1.7,2.5,3.2v8.2c0,2.1-2,3.7-4.1,3.3-18.2-3.5-31.9-19.2-31.9-38s13.7-34.5,31.9-38,4.1,1.2,4.1,3.3Z"/>
|
||||
<path class="st1" d="M378.9,67.9c0,18.9-13.9,34.7-32.3,38-2.1.4-4-1.2-4-3.3v-8.2c0-1.5,1-2.9,2.5-3.3,10.8-2.5,18.7-12,18.7-23.3s-8-20.7-18.7-23.3-2.5-1.7-2.5-3.3v-8.2c0-2.1,1.9-3.7,4-3.3,18.4,3.3,32.3,19.1,32.3,38Z"/>
|
||||
<path class="st1" d="M443.1,37.8c2.3,2.3,4.2,5,5.5,8,0,0,0,.1,0,.2.9,2.2,1.5,4.6,1.8,7.4s-.8,2.1-2,2.1h-10.6c-1,0-1.9-.8-2-1.8-.3-3.3-1.3-4.6-3.2-6.5s-5.1-3.2-7.4-3.6-1.6-1-1.6-1.9v-10.1c0-.9.8-1.7,1.7-1.7h0c7.4,0,12.6,2.9,17.5,7.7"/>
|
||||
<path class="st1" d="M416.8,94.3v9.8c0,1-.9,1.9-1.9,1.9h0c-8.5,0-16-3.9-20.9-9.9,0,0,0,0,0,0-2.4-3-4.2-6.5-5.1-10.3,0-.3-.1-.5-.2-.8,0-.4-.2-.9-.2-1.4,0-.4-.1-.7-.1-1.1,0,0,0-.1,0-.2,0-.9-.1-1.4-.2-2v-.2c0-1.1.9-1.9,2-1.9h10.1c1,0,1.9.8,2,1.8.5,5.1,2.8,8.5,6.6,10.3,1.9.9,4.4,1.6,6.6,2s1.6,1,1.6,1.9Z"/>
|
||||
<path class="st1" d="M451.2,83.5c0,1.3,0,2.5-.2,3.6-.9,5-3.4,9.4-7,12.7-4.1,3.8-9.7,6.1-15.8,6.1h-2.5c-1.1,0-2-.9-2-2v-9.3c0-1,.7-1.8,1.7-1.9s2-.4,2.8-.6c1.8-.6,3.3-1.3,4.6-2.2s2.2-1.9,2.8-3.1c.6-1.2.9-2.3.9-3.5s-.5-2.7-1.4-3.8c-.9-1-2.2-1.9-3.7-2.6s-3.4-1.2-5.5-1.7c-2.1-.5-4.3-.9-6.6-1.3-.6,0-1.2-.2-1.8-.3-1.1-.2-2.2-.4-3.5-.6-2-.4-4.1-.9-6.2-1.5-2.1-.6-4.3-1.5-6.3-2.5-2.1-1-4-2.3-5.6-3.9-1-.9-1.8-1.9-2.6-3.1-.5-.8-1-1.7-1.4-2.6-1-2.2-1.5-4.7-1.5-7.7s.4-5.5,1.2-7.7c0,0,0-.2.1-.3.6-1.3,1.3-2.6,2.1-3.8,0,0,0,0,0,0,0,0,0,0,0,0,4.2-6.1,11-10.2,19-10.2h2c1.1,0,1.9.9,1.9,2v9.6c0,.9-.7,1.8-1.6,1.9s-.5,0-.8.1c-1.6.3-3.1.8-4.5,1.5s-2.6,1.5-3.5,2.6c-1,1.1-1.4,2.3-1.4,3.9,0,1.4.7,2.8,1.8,3.7,1.1,1,2.5,1.8,4.2,2.4,1.7.7,3.6,1.2,5.7,1.7,2.1.5,4.1.9,6,1.4,1.5.3,3.2.7,5.2,1,.9.1,1.7.3,2.6.5,1.1.3,2.3.6,3.5.9,2.1.6,4.2,1.4,6.3,2.4,2.1,1,3.9,2.3,5.6,3.9,1.3,1.2,2.4,2.7,3.3,4.4.2.4.5.9.7,1.4,1,2.3,1.5,5.1,1.5,8.4Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
|
@ -13,10 +13,6 @@ document.addEventListener('alpine:init', () => {
|
|||
// ── Focus (Einzelklick → Vordergrund) ────────────────────────
|
||||
focusedEventId: null,
|
||||
|
||||
// ── Hover-Highlight ──────────────────────────────────────────
|
||||
hoverCol: null,
|
||||
hoverSlot: null,
|
||||
|
||||
// ── Timed-Event DnD (Week / Day) ─────────────────────────────
|
||||
draggingId: null,
|
||||
dragOffsetY: 0,
|
||||
|
|
@ -57,25 +53,6 @@ document.addEventListener('alpine:init', () => {
|
|||
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// HOVER HIGHLIGHT (Week + Day timeline)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
onTimeHover(e, col) {
|
||||
if (this.draggingId) return;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const relY = Math.max(0, e.clientY - rect.top);
|
||||
this.hoverSlot = Math.floor(relY / SLOT_PX);
|
||||
this.hoverCol = col;
|
||||
},
|
||||
|
||||
onTimeLeave(e, col) {
|
||||
if (this.hoverCol === col) {
|
||||
this.hoverCol = null;
|
||||
this.hoverSlot = null;
|
||||
}
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// CREATE ON DOUBLE-CLICK (Week + Day timeline)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -1,10 +1,67 @@
|
|||
@props([
|
||||
'mode' => 'dark',
|
||||
])
|
||||
@php $white = str_contains($mode, 'white'); @endphp
|
||||
<img
|
||||
{{ $attributes }}
|
||||
src="/images/logo.svg"
|
||||
alt="{{ config('app.name') }}"
|
||||
@if($white) style="filter: brightness(0) invert(1);" @endif
|
||||
>
|
||||
|
||||
<svg {{ $attributes }} xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 653 171" version="1.1" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="ArtBoard1" transform="matrix(1,0,0,1.03731,-3508.25,-6783.65)">
|
||||
<rect x="3508.25" y="6539.63" width="652.055" height="163.896" style="fill:none;"/>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M96,13.651C96,6.117 89.883,0 82.349,0L13.651,0C6.117,0 0,6.117 0,13.651L0,82.349C0,89.883 6.117,96 13.651,96L82.349,96C89.883,96 96,89.883 96,82.349L96,13.651Z" style="fill:rgb(15,17,23);"/>
|
||||
</g>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M66,26.343C66,23.946 64.054,22 61.657,22L18.343,22C15.946,22 14,23.946 14,26.343L14,31.657C14,34.054 15.946,36 18.343,36L61.657,36C64.054,36 66,34.054 66,31.657L66,26.343Z" style="fill:rgb(79,70,229);"/>
|
||||
</g>
|
||||
{{-- <g transform="matrix(2.78458,0,0,1.55366,3670.41,6533.52)">--}}
|
||||
{{-- <path class="{{ $mode }}" d="M66,26.343C66,23.946 64.874,22 63.486,22L16.514,22C15.126,22 14,23.946 14,26.343L14,31.657C14,34.054 15.126,36 16.514,36L63.486,36C64.874,36 66,34.054 66,31.657L66,26.343Z" style="fill:rgb(79,70,229);"/>--}}
|
||||
{{-- </g>--}}
|
||||
{{-- <g transform="matrix(0.376057,0,0,1.55366,3862.65,6533.52)">--}}
|
||||
{{-- <path class="{{ $mode }}" d="M66,26.343C66,23.946 57.659,22 47.386,22L32.614,22C22.341,22 14,23.946 14,26.343L14,31.657C14,34.054 22.341,36 32.614,36L47.386,36C57.659,36 66,34.054 66,31.657L66,26.343Z" style="fill:rgb(79,70,229);"/>--}}
|
||||
{{-- </g>--}}
|
||||
{{-- <g transform="matrix(4.66388,0,0,1.55366,3840.19,6533.52)">--}}
|
||||
{{-- <path class="{{ $mode }}" d="M66,26.343C66,23.946 65.327,22 64.499,22L15.501,22C14.673,22 14,23.946 14,26.343L14,31.657C14,34.054 14.673,36 15.501,36L64.499,36C65.327,36 66,34.054 66,31.657L66,26.343Z" style="fill:rgb(79,70,229);"/>--}}
|
||||
{{-- </g>--}}
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M52,46.343C52,43.946 50.054,42 47.657,42L18.343,42C15.946,42 14,43.946 14,46.343L14,51.657C14,54.054 15.946,56 18.343,56L47.657,56C50.054,56 52,54.054 52,51.657L52,46.343Z" style="fill:white;fill-opacity:0.5;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M58,66.343C58,63.946 56.054,62 53.657,62L18.343,62C15.946,62 14,63.946 14,66.343L14,71.657C14,74.054 15.946,76 18.343,76L53.657,76C56.054,76 58,74.054 58,71.657L58,66.343Z" style="fill:white;fill-opacity:0.25;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<circle cx="80" cy="49" r="14" style="fill:rgb(79,70,229);"/>
|
||||
</g>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M78.5,44.551C78.5,43.695 77.805,43 76.949,43L75.051,43C74.195,43 73.5,43.695 73.5,44.551L73.5,53.449C73.5,54.305 74.195,55 75.051,55L76.949,55C77.805,55 78.5,54.305 78.5,53.449L78.5,44.551Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.61164,0,0,1.55366,3520.22,6546.13)">
|
||||
<path d="M85.5,44.551C85.5,43.695 84.805,43 83.949,43L75.051,43C74.195,43 73.5,43.695 73.5,44.551L73.5,46.449C73.5,47.305 74.195,48 75.051,48L83.949,48C84.805,48 85.5,47.305 85.5,46.449L85.5,44.551Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(3.68501,0,0,3.55245,3703.79,6668.63)">
|
||||
<path d="M15.419,-2.394C14.194,-1.441 13.01,-0.727 11.866,-0.25C10.722,0.226 9.438,0.464 8.016,0.464C6.717,0.464 5.576,0.207 4.592,-0.306C3.609,-0.819 2.851,-1.515 2.319,-2.394C1.787,-3.272 1.521,-4.224 1.521,-5.251C1.521,-6.636 1.961,-7.818 2.839,-8.795C3.717,-9.772 4.923,-10.428 6.457,-10.762C6.779,-10.836 7.576,-11.003 8.851,-11.263C10.125,-11.522 11.216,-11.761 12.125,-11.977C13.035,-12.194 14.021,-12.456 15.085,-12.766C15.023,-14.102 14.754,-15.082 14.278,-15.707C13.802,-16.331 12.815,-16.644 11.318,-16.644C10.032,-16.644 9.064,-16.464 8.415,-16.105C7.765,-15.747 7.208,-15.209 6.745,-14.491C6.281,-13.774 5.953,-13.301 5.761,-13.072C5.569,-12.843 5.158,-12.729 4.527,-12.729C3.958,-12.729 3.467,-12.911 3.052,-13.276C2.638,-13.641 2.431,-14.108 2.431,-14.677C2.431,-15.567 2.746,-16.433 3.377,-17.274C4.008,-18.116 4.991,-18.808 6.327,-19.353C7.663,-19.897 9.327,-20.169 11.318,-20.169C13.545,-20.169 15.295,-19.906 16.569,-19.38C17.843,-18.855 18.743,-18.023 19.269,-16.885C19.795,-15.747 20.058,-14.238 20.058,-12.357C20.058,-11.17 20.055,-10.162 20.048,-9.333C20.042,-8.504 20.033,-7.583 20.021,-6.568C20.021,-5.616 20.178,-4.623 20.494,-3.59C20.809,-2.557 20.967,-1.893 20.967,-1.596C20.967,-1.076 20.722,-0.603 20.234,-0.176C19.745,0.25 19.192,0.464 18.573,0.464C18.054,0.464 17.54,0.22 17.033,-0.269C16.526,-0.758 15.988,-1.466 15.419,-2.394ZM15.085,-9.723C14.343,-9.451 13.264,-9.163 11.847,-8.86C10.431,-8.557 9.451,-8.334 8.906,-8.192C8.362,-8.05 7.842,-7.771 7.348,-7.357C6.853,-6.943 6.605,-6.364 6.605,-5.622C6.605,-4.855 6.896,-4.203 7.478,-3.665C8.059,-3.126 8.82,-2.857 9.76,-2.857C10.762,-2.857 11.686,-3.077 12.534,-3.516C13.381,-3.955 14.003,-4.521 14.398,-5.214C14.856,-5.981 15.085,-7.243 15.085,-8.999L15.085,-9.723Z" style="fill:rgb(26,26,46);fill-rule:nonzero;"/>
|
||||
<path d="M38.775,-14.324L29.497,-3.952L39.424,-3.952C40.228,-3.952 40.834,-3.764 41.243,-3.386C41.651,-3.009 41.855,-2.523 41.855,-1.93C41.855,-1.361 41.654,-0.897 41.252,-0.538C40.85,-0.179 40.241,0 39.424,0L25.694,0C24.729,0 24.008,-0.21 23.532,-0.631C23.056,-1.051 22.818,-1.627 22.818,-2.356C22.818,-2.789 22.985,-3.225 23.319,-3.665C23.653,-4.104 24.345,-4.911 25.397,-6.086C26.51,-7.323 27.521,-8.442 28.431,-9.444C29.34,-10.446 30.184,-11.383 30.963,-12.255C31.743,-13.127 32.389,-13.867 32.902,-14.473C33.416,-15.079 33.827,-15.598 34.136,-16.031L26.603,-16.031C25.564,-16.031 24.778,-16.124 24.247,-16.31C23.715,-16.495 23.449,-16.984 23.449,-17.775C23.449,-18.357 23.65,-18.821 24.052,-19.167C24.454,-19.513 25.026,-19.687 25.768,-19.687L37.402,-19.687C38.478,-19.687 39.304,-19.529 39.879,-19.213C40.454,-18.898 40.742,-18.332 40.742,-17.516C40.742,-17.243 40.686,-16.962 40.575,-16.671C40.463,-16.381 40.34,-16.143 40.204,-15.957C40.067,-15.771 39.882,-15.546 39.647,-15.28C39.412,-15.014 39.121,-14.695 38.775,-14.324Z" style="fill:rgb(26,26,46);fill-rule:nonzero;"/>
|
||||
<path class="{{ $mode }}" d="M49.681,-17.256L49.681,-2.616C49.681,-1.602 49.439,-0.835 48.957,-0.315C48.474,0.204 47.862,0.464 47.12,0.464C46.378,0.464 45.775,0.198 45.311,-0.334C44.847,-0.866 44.615,-1.627 44.615,-2.616L44.615,-17.107C44.615,-18.109 44.847,-18.864 45.311,-19.371C45.775,-19.878 46.378,-20.132 47.12,-20.132C47.862,-20.132 48.474,-19.878 48.957,-19.371C49.439,-18.864 49.681,-18.159 49.681,-17.256ZM47.176,-22.488C46.471,-22.488 45.868,-22.705 45.367,-23.138C44.866,-23.571 44.615,-24.183 44.615,-24.975C44.615,-25.692 44.872,-26.283 45.385,-26.747C45.898,-27.21 46.495,-27.442 47.176,-27.442C47.831,-27.442 48.413,-27.232 48.92,-26.812C49.427,-26.391 49.681,-25.779 49.681,-24.975C49.681,-24.195 49.433,-23.586 48.938,-23.147C48.444,-22.708 47.856,-22.488 47.176,-22.488Z" style="fill:rgb(79,70,229);fill-rule:nonzero;"/>
|
||||
<path class="{{ $mode }}" d="M59.751,-6.847L59.751,-2.616C59.751,-1.59 59.51,-0.819 59.028,-0.306C58.545,0.207 57.933,0.464 57.191,0.464C56.461,0.464 55.861,0.204 55.391,-0.315C54.921,-0.835 54.686,-1.602 54.686,-2.616L54.686,-16.718C54.686,-18.994 55.508,-20.132 57.154,-20.132C57.995,-20.132 58.601,-19.866 58.972,-19.334C59.343,-18.802 59.547,-18.017 59.584,-16.978C60.19,-18.017 60.812,-18.802 61.449,-19.334C62.086,-19.866 62.937,-20.132 64,-20.132C65.064,-20.132 66.097,-19.866 67.099,-19.334C68.101,-18.802 68.602,-18.097 68.602,-17.219C68.602,-16.6 68.388,-16.09 67.962,-15.688C67.535,-15.286 67.074,-15.085 66.579,-15.085C66.394,-15.085 65.945,-15.199 65.234,-15.428C64.523,-15.657 63.895,-15.771 63.351,-15.771C62.609,-15.771 62.003,-15.577 61.533,-15.187C61.062,-14.797 60.698,-14.219 60.438,-13.452C60.178,-12.685 59.999,-11.773 59.9,-10.715C59.801,-9.658 59.751,-8.368 59.751,-6.847Z" style="fill:rgb(79,70,229);fill-rule:nonzero;"/>
|
||||
<path class="{{ $mode }}" d="M88.692,-9.834C88.692,-8.325 88.457,-6.933 87.987,-5.659C87.517,-4.385 86.837,-3.29 85.946,-2.375C85.055,-1.46 83.992,-0.758 82.755,-0.269C81.518,0.22 80.126,0.464 78.58,0.464C77.046,0.464 75.667,0.216 74.442,-0.278C73.218,-0.773 72.157,-1.481 71.26,-2.403C70.363,-3.324 69.683,-4.413 69.219,-5.668C68.755,-6.924 68.523,-8.313 68.523,-9.834C68.523,-11.368 68.758,-12.772 69.228,-14.046C69.698,-15.32 70.372,-16.409 71.251,-17.312C72.129,-18.215 73.193,-18.91 74.442,-19.399C75.691,-19.888 77.071,-20.132 78.58,-20.132C80.114,-20.132 81.505,-19.884 82.755,-19.39C84.004,-18.895 85.074,-18.19 85.965,-17.274C86.855,-16.359 87.532,-15.271 87.996,-14.009C88.46,-12.747 88.692,-11.355 88.692,-9.834ZM83.608,-9.834C83.608,-11.9 83.154,-13.508 82.244,-14.658C81.335,-15.809 80.114,-16.384 78.58,-16.384C77.59,-16.384 76.718,-16.127 75.964,-15.614C75.209,-15.1 74.628,-14.343 74.219,-13.341C73.811,-12.339 73.607,-11.17 73.607,-9.834C73.607,-8.51 73.808,-7.354 74.21,-6.364C74.612,-5.375 75.187,-4.617 75.936,-4.091C76.684,-3.566 77.566,-3.303 78.58,-3.303C80.114,-3.303 81.335,-3.881 82.244,-5.038C83.154,-6.194 83.608,-7.793 83.608,-9.834Z" style="fill:rgb(79,70,229);fill-rule:nonzero;"/>
|
||||
<path class="{{ $mode }}" d="M108.801,-6.16C108.801,-4.762 108.461,-3.566 107.78,-2.57C107.1,-1.574 106.095,-0.819 104.765,-0.306C103.436,0.207 101.818,0.464 99.913,0.464C98.095,0.464 96.536,0.186 95.238,-0.371C93.939,-0.928 92.98,-1.624 92.362,-2.458C91.743,-3.293 91.434,-4.132 91.434,-4.973C91.434,-5.529 91.632,-6.006 92.028,-6.401C92.423,-6.797 92.924,-6.995 93.53,-6.995C94.062,-6.995 94.471,-6.865 94.755,-6.605C95.04,-6.346 95.312,-5.981 95.571,-5.511C96.091,-4.608 96.713,-3.934 97.436,-3.488C98.16,-3.043 99.146,-2.82 100.396,-2.82C101.41,-2.82 102.242,-3.046 102.891,-3.498C103.541,-3.949 103.865,-4.465 103.865,-5.047C103.865,-5.938 103.528,-6.587 102.854,-6.995C102.18,-7.403 101.07,-7.793 99.524,-8.164C97.779,-8.597 96.36,-9.052 95.265,-9.528C94.171,-10.004 93.295,-10.632 92.64,-11.411C91.984,-12.19 91.656,-13.149 91.656,-14.287C91.656,-15.301 91.96,-16.26 92.566,-17.163C93.172,-18.066 94.065,-18.787 95.247,-19.325C96.428,-19.863 97.854,-20.132 99.524,-20.132C100.835,-20.132 102.013,-19.996 103.058,-19.724C104.104,-19.451 104.976,-19.087 105.675,-18.629C106.373,-18.171 106.905,-17.664 107.27,-17.107C107.635,-16.551 107.818,-16.007 107.818,-15.475C107.818,-14.893 107.623,-14.417 107.233,-14.046C106.843,-13.675 106.29,-13.489 105.572,-13.489C105.053,-13.489 104.611,-13.638 104.246,-13.935C103.881,-14.231 103.463,-14.677 102.993,-15.271C102.61,-15.765 102.158,-16.161 101.639,-16.458C101.119,-16.755 100.414,-16.903 99.524,-16.903C98.608,-16.903 97.848,-16.708 97.241,-16.319C96.635,-15.929 96.332,-15.444 96.332,-14.862C96.332,-14.33 96.555,-13.894 97,-13.554C97.446,-13.214 98.045,-12.933 98.8,-12.71C99.555,-12.487 100.594,-12.215 101.917,-11.894C103.488,-11.51 104.772,-11.052 105.767,-10.521C106.763,-9.989 107.518,-9.361 108.031,-8.637C108.544,-7.914 108.801,-7.088 108.801,-6.16Z" style="fill:rgb(79,70,229);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(3.68501,0,0,3.55245,3142,6474.89)">
|
||||
<circle class="{{ $mode }}" cx="268" cy="50" r="5" style="fill:rgb(79,70,229);"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{{--<svg {{ $attributes }} width="240" height="52" viewBox="0 0 240 52" xmlns="http://www.w3.org/2000/svg">--}}
|
||||
{{-- <circle cx="26" cy="26" r="18" fill="none" stroke="#4F46E5" stroke-width="2.2"/>--}}
|
||||
{{-- <rect x="17" y="17" width="18" height="18" rx="2.5" fill="#4F46E5" transform="rotate(45 26 26)"/>--}}
|
||||
{{-- <circle cx="26" cy="26" r="5" fill="#f4f4f8"/>--}}
|
||||
{{-- <path d="M60 35 Q60 21 71 21 Q82 21 82 32 L82 35" fill="none" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>--}}
|
||||
{{-- <line x1="82" y1="26" x2="82" y2="35" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <line x1="89" y1="21" x2="104" y2="21" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <line x1="104" y1="21" x2="89" y2="35" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <line x1="89" y1="35" x2="104" y2="35" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <line x1="112" y1="21" x2="112" y2="35" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <circle cx="112" cy="16" r="2.5" fill="#4F46E5"/>--}}
|
||||
{{-- <line x1="120" y1="21" x2="120" y2="35" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <path d="M120 26 Q127 21 135 23" fill="none" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{-- <ellipse cx="149" cy="28" rx="9.5" ry="8" fill="none" stroke="#1a1a2e" stroke-width="2.2"/>--}}
|
||||
{{-- <path d="M169 23 Q169 21 175 21 Q182 21 182 26 Q182 31 175 28 Q168 25 168 30 Q168 35 175 35 Q182 35 182 33" fill="none" stroke="#1a1a2e" stroke-width="2.2" stroke-linecap="round"/>--}}
|
||||
{{--</svg>--}}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,26 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-star style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Dein Referral hat sich qualifiziert!
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
<strong style="color:#374151;">{{ $referredUser->name }}</strong> nutzt Aziros jetzt seit 3 Monaten.
|
||||
</p>
|
||||
|
||||
{{-- Credits badge --}}
|
||||
<div style="background:#EEF2FF;border:1px solid #C7D2FE;border-radius:12px;padding:22px 20px;margin-bottom:28px;text-align:center;">
|
||||
<p style="margin:0 0 4px;font-size:11px;color:#818CF8;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">Gutschrift</p>
|
||||
<p style="margin:0 0 4px;font-size:36px;font-weight:800;color:#4F46E5;line-height:1;letter-spacing:-1px;">+{{ $credits }}</p>
|
||||
<p style="margin:0;font-size:13px;color:#6B7280;">Credits wurden deinem Konto gutgeschrieben</p>
|
||||
<div style="text-align:center;margin-bottom:20px;">
|
||||
<x-heroicon-o-gift width="65"
|
||||
style="opacity:0.8;stroke:#4f46e5;stroke-width:1" />
|
||||
</div>
|
||||
|
||||
{{-- CTA --}}
|
||||
<x-mail.button :url="config('app.url') . '/settings?tab=affiliate'">
|
||||
<h2 style="margin:0 0 10px;font-size:22px;color:#111;text-align:center;">
|
||||
Du hast {{ $credits }} Credits verdient!
|
||||
</h2>
|
||||
|
||||
<p style="margin:0 0 20px;color:#6b7280;font-size:14px;text-align:center;line-height:1.6;">
|
||||
<strong>{{ $referredUser->name }}</strong> nutzt Aziros jetzt seit 3 Monaten
|
||||
— dein Referral hat sich qualifiziert!
|
||||
</p>
|
||||
|
||||
<div style="background:#EEF2FF;border-radius:12px;padding:20px;margin:24px 0;text-align:center;">
|
||||
<div style="font-size:36px;font-weight:700;color:#4F46E5;">+{{ $credits }}</div>
|
||||
<div style="font-size:14px;color:#6B7280;">Credits wurden deinem Konto gutgeschrieben</div>
|
||||
</div>
|
||||
|
||||
<x-mail.button url="{{ config('app.url') }}/settings?tab=affiliate">
|
||||
Mein Affiliate-Dashboard
|
||||
</x-mail.button>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,31 +2,25 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-chat-bubble-left-right style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align:center;margin-bottom:20px;">
|
||||
<x-heroicon-o-chat-bubble-left-right width="55"
|
||||
style="opacity:0.8;stroke:#4f46e5;stroke-width:1" />
|
||||
</div>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 4px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Neue Nachricht
|
||||
</h1>
|
||||
<p style="margin:0 0 28px;font-size:13px;color:#9CA3AF;text-align:center;">
|
||||
<h2 style="margin:0 0 6px;font-size:22px;color:#111;text-align:center;">
|
||||
Nachricht
|
||||
</h2>
|
||||
|
||||
<p style="margin:0 0 24px;color:#9ca3af;font-size:13px;text-align:center;">
|
||||
von {{ $sender_name }}
|
||||
</p>
|
||||
|
||||
{{-- Recipient label --}}
|
||||
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">
|
||||
<p style="margin:0 0 6px;font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">
|
||||
Nachricht an {{ $recipient_name }}
|
||||
</p>
|
||||
|
||||
{{-- Message box --}}
|
||||
<div style="background:#F9FAFB;border:1px solid #F3F4F6;border-radius:12px;padding:20px;margin-bottom:0;">
|
||||
<p style="margin:0;font-size:14px;color:#374151;line-height:1.7;">{{ $message }}</p>
|
||||
<div style="background:#f8fafc;border-radius:12px;padding:20px;margin-bottom:20px;">
|
||||
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">{{ $message }}</p>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -2,70 +2,55 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-calendar-days style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align:center;margin-bottom:20px;">
|
||||
<x-heroicon-o-calendar-days width="55"
|
||||
style="opacity:0.8;stroke:#4f46e5;stroke-width:1" />
|
||||
</div>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 4px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
<h2 style="margin:0 0 6px;font-size:22px;color:#111;text-align:center;">
|
||||
Terminerinnerung
|
||||
</h1>
|
||||
<p style="margin:0 0 28px;font-size:13px;color:#9CA3AF;text-align:center;">
|
||||
</h2>
|
||||
|
||||
<p style="margin:0 0 24px;color:#9ca3af;font-size:13px;text-align:center;">
|
||||
von {{ $sender_name }}
|
||||
</p>
|
||||
|
||||
{{-- Event card --}}
|
||||
<div style="background:#F9FAFB;border:1px solid #F3F4F6;border-radius:12px;padding:20px 20px 16px;margin-bottom:20px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
{{-- Event Details --}}
|
||||
<div style="background:#f8fafc;border-radius:12px;padding:20px;margin-bottom:20px;">
|
||||
<table width="100%" style="font-size:14px;color:#374151;">
|
||||
<tr>
|
||||
<td style="padding:5px 0 5px;width:72px;font-size:12px;color:#9CA3AF;vertical-align:top;">Termin</td>
|
||||
<td style="padding:5px 0 5px;font-size:14px;color:#111827;font-weight:600;">{{ $event_title }}</td>
|
||||
<td style="padding:6px 0;color:#9ca3af;width:80px;">Termin</td>
|
||||
<td style="padding:6px 0;font-weight:600;">{{ $event_title }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:5px 0;height:1px;" colspan="2">
|
||||
<div style="height:1px;background:#F3F4F6;"></div>
|
||||
</td>
|
||||
<td style="padding:6px 0;color:#9ca3af;">Datum</td>
|
||||
<td style="padding:6px 0;">{{ $event_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:5px 0;font-size:12px;color:#9CA3AF;vertical-align:top;">Datum</td>
|
||||
<td style="padding:5px 0;font-size:13px;color:#374151;">{{ $event_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:5px 0;font-size:12px;color:#9CA3AF;vertical-align:top;">Uhrzeit</td>
|
||||
<td style="padding:5px 0;font-size:13px;color:#374151;">
|
||||
{{ $event_time }}@if(!empty($event_end)) – {{ $event_end }}@endif Uhr
|
||||
<td style="padding:6px 0;color:#9ca3af;">Uhrzeit</td>
|
||||
<td style="padding:6px 0;">
|
||||
{{ $event_time }}@if(!empty($event_end)) - {{ $event_end }}@endif Uhr
|
||||
</td>
|
||||
</tr>
|
||||
@if(!empty($event_notes))
|
||||
<tr>
|
||||
<td style="padding:5px 0;height:1px;" colspan="2">
|
||||
<div style="height:1px;background:#F3F4F6;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:5px 0;font-size:12px;color:#9CA3AF;vertical-align:top;">Info</td>
|
||||
<td style="padding:5px 0;font-size:13px;color:#374151;line-height:1.5;">{{ $event_notes }}</td>
|
||||
<td style="padding:6px 0;color:#9ca3af;vertical-align:top;">Info</td>
|
||||
<td style="padding:6px 0;">{{ $event_notes }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Personal message --}}
|
||||
{{-- Persönliche Nachricht --}}
|
||||
@if(!empty($message))
|
||||
<div style="border-left:3px solid #4F46E5;padding:12px 16px;background:#F9FAFB;border-radius:0 10px 10px 0;margin-bottom:24px;">
|
||||
<p style="margin:0 0 3px;font-size:10px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">Nachricht</p>
|
||||
<p style="margin:0;font-size:13px;color:#374151;line-height:1.6;">{{ $message }}</p>
|
||||
<div style="border-left:3px solid #4f46e5;padding:12px 16px;margin-bottom:20px;background:#f8fafc;border-radius:0 8px 8px 0;">
|
||||
<p style="margin:0 0 4px;font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Nachricht</p>
|
||||
<p style="margin:0;font-size:14px;color:#374151;line-height:1.5;">{{ $message }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Recipient note --}}
|
||||
<p style="margin:0;text-align:center;font-size:11px;color:#D1D5DB;line-height:1.6;">
|
||||
Hallo {{ $recipient_name }} — dies ist eine automatische Terminerinnerung.
|
||||
<p style="text-align:center;font-size:12px;color:#d1d5db;margin-top:24px;">
|
||||
Hallo {{ $recipient_name }}, dies ist eine automatische Terminerinnerung.
|
||||
</p>
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-sparkles style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Message body --}}
|
||||
<div style="font-size:14px;color:#374151;line-height:1.7;">
|
||||
{!! nl2br(e($body)) !!}
|
||||
</div>
|
||||
|
||||
{{-- Sent by footer --}}
|
||||
<div style="margin-top:24px;padding-top:18px;border-top:1px solid #F3F4F6;">
|
||||
<p style="margin:0;font-size:11px;color:#D1D5DB;text-align:center;letter-spacing:0.2px;">
|
||||
Gesendet von <strong style="color:#9CA3AF;">Aria</strong> · aziros.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
<p>{!! nl2br(e($body)) !!}</p>
|
||||
<hr style="margin: 24px 0; border: none; border-top: 1px solid #eee;">
|
||||
<p style="font-size: 12px; color: #999;">Gesendet von Aria · aziros.com</p>
|
||||
|
|
|
|||
|
|
@ -2,32 +2,27 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-envelope style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="text-align:center;margin-bottom:20px;">
|
||||
<x-heroicon-o-envelope width="65"
|
||||
style="opacity:0.8;stroke:#4f46e5;stroke-width:1" />
|
||||
</div>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
<h2 style="margin:0 0 10px;font-size:22px;color:#111;text-align:center;">
|
||||
{{ t('mail.auth.verify.title') }}
|
||||
</h1>
|
||||
<p style="margin:0 0 28px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
</h2>
|
||||
|
||||
<p style="margin:0 0 20px;color:#6b7280;font-size:14px;text-align:center;">
|
||||
{{ t('mail.auth.verify.text') }}
|
||||
</p>
|
||||
|
||||
{{-- Button --}}
|
||||
{{-- BUTTON --}}
|
||||
@if(!empty($url))
|
||||
<x-mail.button :url="$url">
|
||||
{{ t('mail.auth.verify.button') }}
|
||||
</x-mail.button>
|
||||
@endif
|
||||
|
||||
{{-- Expiry --}}
|
||||
<p style="margin:20px 0 0;text-align:center;font-size:12px;color:#9CA3AF;line-height:1.6;">
|
||||
<p style="text-align:center;font-size:13px;color:#9ca3af;">
|
||||
{{ t('mail.auth.verify.expires') }}
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-hand-raised style="width:24px;height:24px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Willkommen bei Aziros, {{ $user->name }}!
|
||||
</h1>
|
||||
<p style="margin:0 0 28px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Dein Konto ist bestätigt. Hier ist, was du als Nächstes tun kannst.
|
||||
</p>
|
||||
|
||||
{{-- Feature list --}}
|
||||
<div style="background:#F9FAFB;border:1px solid #F3F4F6;border-radius:12px;padding:20px 20px 16px;margin-bottom:28px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td style="padding:8px 0;">
|
||||
<table cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td style="width:28px;vertical-align:top;padding-top:1px;">
|
||||
<div style="width:20px;height:20px;background:#EEF2FF;border-radius:6px;text-align:center;line-height:20px;">
|
||||
<x-heroicon-o-calendar-days style="width:11px;height:11px;stroke:#4F46E5;stroke-width:2;display:inline-block;vertical-align:middle;" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding-left:10px;">
|
||||
<p style="margin:0 0 2px;font-size:13px;font-weight:600;color:#111827;">Kalender</p>
|
||||
<p style="margin:0;font-size:12px;color:#6B7280;">Termine planen, organisieren und im Blick behalten.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td style="height:1px;background:#F3F4F6;padding:0;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;">
|
||||
<table cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td style="width:28px;vertical-align:top;padding-top:1px;">
|
||||
<div style="width:20px;height:20px;background:#EEF2FF;border-radius:6px;text-align:center;line-height:20px;">
|
||||
<x-heroicon-o-sparkles style="width:11px;height:11px;stroke:#4F46E5;stroke-width:2;display:inline-block;vertical-align:middle;" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding-left:10px;">
|
||||
<p style="margin:0 0 2px;font-size:13px;font-weight:600;color:#111827;">Aria – Dein KI-Assistent</p>
|
||||
<p style="margin:0;font-size:12px;color:#6B7280;">Frag Aria alles – sie hilft dir, deinen Tag zu organisieren.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td style="height:1px;background:#F3F4F6;padding:0;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;">
|
||||
<table cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td style="width:28px;vertical-align:top;padding-top:1px;">
|
||||
<div style="width:20px;height:20px;background:#EEF2FF;border-radius:6px;text-align:center;line-height:20px;">
|
||||
<x-heroicon-o-arrows-right-left style="width:11px;height:11px;stroke:#4F46E5;stroke-width:2;display:inline-block;vertical-align:middle;" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding-left:10px;">
|
||||
<p style="margin:0 0 2px;font-size:13px;font-weight:600;color:#111827;">Integrationen</p>
|
||||
<p style="margin:0;font-size:12px;color:#6B7280;">Google Kalender & Outlook verbinden und synchronisieren.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- CTA --}}
|
||||
<x-mail.button :url="config('app.url')">
|
||||
Jetzt loslegen
|
||||
</x-mail.button>
|
||||
|
||||
@endsection
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
<p style="margin:28px 0 0;text-align:center;">
|
||||
<div style="text-align:center;margin-top:10px;">
|
||||
<a href="{{ $url }}"
|
||||
style="display:inline-block;background:#4F46E5;color:#ffffff;padding:14px 32px;border-radius:10px;text-decoration:none;font-size:14px;font-weight:600;letter-spacing:0.1px;line-height:1;">
|
||||
style="
|
||||
display:inline-block;
|
||||
padding:12px 20px;
|
||||
background:#4f46e5;
|
||||
color:#fff;
|
||||
border-radius:8px;
|
||||
text-decoration:none;
|
||||
font-size:14px;
|
||||
">
|
||||
{{ $slot }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:28px auto;">
|
||||
<table align="center" cellpadding="0" cellspacing="0" style="margin:25px auto;">
|
||||
<tr>
|
||||
@foreach(str_split($slot) as $digit)
|
||||
<td align="center"
|
||||
style="width:46px;height:56px;background:#F4F6FB;border:1px solid #E5E7EB;border-radius:12px;font-size:24px;font-weight:700;color:#111827;letter-spacing:-0.5px;">
|
||||
@foreach(str_split($slot) as $index => $digit)
|
||||
|
||||
<td align="center" style="
|
||||
width:44px;
|
||||
height:52px;
|
||||
font-size:22px;
|
||||
font-weight:600;
|
||||
color:#111;
|
||||
background:#f3f4f6;
|
||||
border-radius:10px;
|
||||
box-shadow:0 2px 6px rgba(0,0,0,0.05);
|
||||
">
|
||||
{{ $digit }}
|
||||
</td>
|
||||
|
||||
{{-- Abstand --}}
|
||||
@if(!$loop->last)
|
||||
<td width="8"></td>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -2,37 +2,33 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#F0FDF4;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-gift style="width:24px;height:24px;stroke:#10B981;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Hallo {{ $user->name }}!
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Du hast kostenlosen Zugang zum
|
||||
<strong style="color:#4F46E5;">{{ $plan->name }}</strong>-Plan erhalten — {{ $durationLabel }}.
|
||||
</p>
|
||||
|
||||
{{-- Validity badge --}}
|
||||
<div style="background:#F0FDF4;border:1px solid #D1FAE5;border-radius:12px;padding:18px 20px;margin-bottom:28px;text-align:center;">
|
||||
@if($endsAt)
|
||||
<p style="margin:0 0 3px;font-size:11px;color:#6EE7B7;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">Gültig bis</p>
|
||||
<p style="margin:0;font-size:20px;font-weight:700;color:#059669;">{{ $endsAt->format('d.m.Y') }}</p>
|
||||
@else
|
||||
<p style="margin:0 0 3px;font-size:11px;color:#6EE7B7;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">Gültigkeit</p>
|
||||
<p style="margin:0;font-size:18px;font-weight:700;color:#059669;">Unbegrenzt</p>
|
||||
@endif
|
||||
<div style="text-align:center;margin-bottom:20px;">
|
||||
<x-heroicon-o-gift width="65"
|
||||
style="opacity:0.8;stroke:#4f46e5;stroke-width:1" />
|
||||
</div>
|
||||
|
||||
{{-- CTA --}}
|
||||
<x-mail.button :url="config('app.url') . '/agent'">
|
||||
<h2 style="margin:0 0 10px;font-size:22px;color:#111;text-align:center;">
|
||||
Hallo {{ $user->name }}!
|
||||
</h2>
|
||||
|
||||
<p style="margin:0 0 20px;color:#6b7280;font-size:14px;text-align:center;line-height:1.6;">
|
||||
Du hast kostenlosen Zugang zum
|
||||
<strong style="color:#4F46E5;">{{ $plan->name }}</strong>
|
||||
erhalten — {{ $durationLabel }}.
|
||||
</p>
|
||||
|
||||
@if($endsAt)
|
||||
<p style="margin:0 0 20px;color:#6b7280;font-size:13px;text-align:center;">
|
||||
Dein Zugang ist gültig bis:
|
||||
<strong>{{ $endsAt->format('d.m.Y') }}</strong>
|
||||
</p>
|
||||
@else
|
||||
<p style="margin:0 0 20px;color:#6b7280;font-size:13px;text-align:center;">
|
||||
Dein Zugang ist <strong>unbegrenzt gültig</strong>.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<x-mail.button url="{{ config('app.url') }}/agent">
|
||||
Jetzt Aria nutzen
|
||||
</x-mail.button>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Aziros</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#F4F6FB;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;">
|
||||
<html>
|
||||
<body style="margin:0;background:#f4f6fb;font-family:Arial,sans-serif;">
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:#F4F6FB;padding:48px 16px;">
|
||||
<tr><td align="center">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="max-width:560px;">
|
||||
|
||||
{{-- Logo --}}
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td style="padding-bottom:20px;padding-left:2px;">
|
||||
<a href="{{ config('app.url') }}" style="text-decoration:none;display:inline-block;">
|
||||
<img src="{{ config('app.url') }}/images/logo-text.png" height="32" alt="aziros" style="display:block;height:32px;width:auto;border:0;">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<td align="center">
|
||||
|
||||
{{-- Card --}}
|
||||
<table width="100%" style="max-width:520px;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 20px 40px rgba(0,0,0,0.08);">
|
||||
|
||||
{{-- HEADER --}}
|
||||
@include('emails.components.header')
|
||||
|
||||
{{-- CONTENT --}}
|
||||
<tr>
|
||||
<td style="background:#ffffff;border:1px solid #E5E7EB;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.06),0 4px 12px rgba(0,0,0,0.04);">
|
||||
|
||||
{{-- Accent bar --}}
|
||||
<div style="height:3px;background:linear-gradient(90deg,#4F46E5 0%,#818CF8 55%,#C7D2FE 100%);font-size:0;line-height:3px;">‌</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div style="padding:40px 40px 36px;">
|
||||
<td style="padding:32px;">
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Footer --}}
|
||||
<tr>
|
||||
<td style="padding-top:28px;text-align:center;">
|
||||
<p style="margin:0 0 4px;font-size:12px;color:#9CA3AF;line-height:1.7;">
|
||||
Du erhältst diese E-Mail, weil du <strong style="font-weight:500;color:#6B7280;">{{ config('app.name') }}</strong> nutzt.<br>
|
||||
Falls du sie nicht erwartet hast, kannst du sie ignorieren.
|
||||
</p>
|
||||
<p style="margin:0;font-size:11px;color:#D1D5DB;letter-spacing:0.2px;">
|
||||
© {{ date('Y') }} Aziros GmbH · Made with ♡ in Austria
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{-- FOOTER --}}
|
||||
@include('emails.components.footer')
|
||||
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,63 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Passwort zurücksetzen</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#F9FAFB;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#F9FAFB;padding:40px 16px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#EEF2FF;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-lock-closed style="width:22px;height:22px;stroke:#4F46E5;stroke-width:1.5;display:block;margin:15px auto;" />
|
||||
<td align="center">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;">
|
||||
|
||||
{{-- Logo --}}
|
||||
<tr>
|
||||
<td style="padding-bottom:24px;">
|
||||
<span style="font-size:22px;font-weight:300;color:#0D0D18;letter-spacing:-0.5px;">aziros</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Card --}}
|
||||
<tr>
|
||||
<td style="background:#fff;border-radius:16px;border:1px solid #E5E7EB;padding:40px;">
|
||||
|
||||
<p style="margin:0 0 8px;font-size:18px;font-weight:600;color:#111827;">
|
||||
Passwort zurücksetzen
|
||||
</p>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;">
|
||||
Hallo {{ $user->name }},
|
||||
</p>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#374151;line-height:1.6;">
|
||||
Du hast ein Zurücksetzen deines Passworts angefordert.
|
||||
Klicke auf den Button, um ein neues Passwort zu setzen.
|
||||
</p>
|
||||
|
||||
<a href="{{ $url }}"
|
||||
style="display:inline-block;background:#4F46E5;color:#fff;
|
||||
padding:14px 28px;border-radius:10px;
|
||||
text-decoration:none;font-size:14px;font-weight:600;">
|
||||
Passwort zurücksetzen
|
||||
</a>
|
||||
|
||||
<p style="margin:24px 0 0;font-size:12px;color:#9CA3AF;line-height:1.6;">
|
||||
Dieser Link ist <strong>60 Minuten</strong> gültig.<br>
|
||||
Falls du kein Passwort-Reset angefordert hast, ignoriere diese E-Mail.
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Footer --}}
|
||||
<tr>
|
||||
<td style="padding-top:24px;text-align:center;font-size:12px;color:#9CA3AF;">
|
||||
© {{ date('Y') }} Aziros · Made in Austria
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Passwort zurücksetzen
|
||||
</h1>
|
||||
<p style="margin:0 0 28px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Hallo {{ $user->name }}, klicke auf den Button um ein neues Passwort zu setzen.
|
||||
</p>
|
||||
|
||||
{{-- Button --}}
|
||||
<x-mail.button :url="$url">
|
||||
Passwort zurücksetzen
|
||||
</x-mail.button>
|
||||
|
||||
{{-- Expiry note --}}
|
||||
<div style="margin-top:24px;background:#F9FAFB;border:1px solid #F3F4F6;border-radius:10px;padding:14px 16px;text-align:center;">
|
||||
<p style="margin:0;font-size:12px;color:#9CA3AF;line-height:1.7;">
|
||||
Dieser Link ist <strong style="color:#6B7280;">60 Minuten</strong> gültig.<br>
|
||||
Falls du kein Passwort-Reset angefordert hast, ignoriere diese E-Mail.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,2 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#F0FDF4;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-check-circle style="width:24px;height:24px;stroke:#10B981;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
SMTP erfolgreich konfiguriert
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Dein SMTP-Server ist korrekt eingerichtet.<br>
|
||||
E-Mails können von Aria in deinem Namen versendet werden.
|
||||
</p>
|
||||
|
||||
{{-- Status box --}}
|
||||
<div style="background:#F0FDF4;border:1px solid #D1FAE5;border-radius:10px;padding:14px 18px;text-align:center;">
|
||||
<p style="margin:0;font-size:13px;color:#059669;font-weight:500;">
|
||||
✓ Verbindung hergestellt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
<p>Dein SMTP Server ist korrekt konfiguriert.</p>
|
||||
<p>E-Mails können von Aria in deinem Namen gesendet werden.</p>
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#FFF7ED;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-x-circle style="width:24px;height:24px;stroke:#F97316;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Dein Abo wurde gekündigt
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Hallo {{ $user->name }}, wir haben deine Kündigung des <strong style="color:#111827;">{{ $plan }}</strong>-Plans erhalten.
|
||||
</p>
|
||||
|
||||
{{-- Access info --}}
|
||||
@if(!empty($endsAt))
|
||||
<div style="background:#FFF7ED;border:1px solid #FED7AA;border-radius:12px;padding:18px 20px;margin-bottom:28px;text-align:center;">
|
||||
<p style="margin:0 0 4px;font-size:11px;color:#FB923C;text-transform:uppercase;letter-spacing:0.6px;font-weight:600;">Zugang aktiv bis</p>
|
||||
<p style="margin:0;font-size:20px;font-weight:700;color:#EA580C;">{{ $endsAt->format('d.m.Y') }}</p>
|
||||
<p style="margin:6px 0 0;font-size:12px;color:#9CA3AF;">Danach wechselst du automatisch auf den kostenlosen Plan.</p>
|
||||
</div>
|
||||
@else
|
||||
<div style="background:#FFF7ED;border:1px solid #FED7AA;border-radius:12px;padding:16px 20px;margin-bottom:28px;text-align:center;">
|
||||
<p style="margin:0;font-size:13px;color:#EA580C;">Du wechselst sofort auf den kostenlosen Plan.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Reactivate CTA --}}
|
||||
<x-mail.button :url="config('app.url') . '/plans'">
|
||||
Abo reaktivieren
|
||||
</x-mail.button>
|
||||
|
||||
{{-- Footer note --}}
|
||||
<p style="margin:20px 0 0;text-align:center;font-size:12px;color:#9CA3AF;line-height:1.6;">
|
||||
Falls du eine Frage hast, antworte einfach auf diese E-Mail.
|
||||
</p>
|
||||
|
||||
@endsection
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
@extends('emails.layout')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Icon --}}
|
||||
<table cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 28px;">
|
||||
<tr>
|
||||
<td style="width:52px;height:52px;background:#F0FDF4;border-radius:14px;text-align:center;vertical-align:middle;">
|
||||
<x-heroicon-o-check-badge style="width:24px;height:24px;stroke:#10B981;stroke-width:1.5;display:block;margin:14px auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 style="margin:0 0 8px;font-size:22px;font-weight:700;color:#111827;text-align:center;line-height:1.3;letter-spacing:-0.3px;">
|
||||
Abo erfolgreich aktiviert!
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;font-size:14px;color:#6B7280;text-align:center;line-height:1.6;">
|
||||
Hallo {{ $user->name }}, dein <strong style="color:#111827;">{{ $plan }}</strong>-Plan ist jetzt aktiv.
|
||||
</p>
|
||||
|
||||
{{-- Details card --}}
|
||||
<div style="background:#F9FAFB;border:1px solid #F3F4F6;border-radius:12px;padding:20px 20px 16px;margin-bottom:28px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td style="padding:6px 0;font-size:12px;color:#9CA3AF;width:130px;">Plan</td>
|
||||
<td style="padding:6px 0;font-size:13px;font-weight:600;color:#111827;">{{ $plan }}</td>
|
||||
</tr>
|
||||
<tr><td colspan="2" style="height:1px;background:#F3F4F6;padding:0;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:6px 0;font-size:12px;color:#9CA3AF;">Abrechnungszeitraum</td>
|
||||
<td style="padding:6px 0;font-size:13px;color:#374151;">{{ $billing === 'yearly' ? 'Jährlich' : 'Monatlich' }}</td>
|
||||
</tr>
|
||||
<tr><td colspan="2" style="height:1px;background:#F3F4F6;padding:0;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:6px 0;font-size:12px;color:#9CA3AF;">Betrag</td>
|
||||
<td style="padding:6px 0;font-size:13px;color:#374151;">{{ number_format($amount / 100, 2, ',', '.') }} € / {{ $billing === 'yearly' ? 'Jahr' : 'Monat' }}</td>
|
||||
</tr>
|
||||
@if(!empty($renewsAt))
|
||||
<tr><td colspan="2" style="height:1px;background:#F3F4F6;padding:0;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding:6px 0;font-size:12px;color:#9CA3AF;">Nächste Verlängerung</td>
|
||||
<td style="padding:6px 0;font-size:13px;color:#374151;">{{ $renewsAt->format('d.m.Y') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- CTA --}}
|
||||
<x-mail.button :url="config('app.url')">
|
||||
Zur App
|
||||
</x-mail.button>
|
||||
|
||||
{{-- Footer note --}}
|
||||
<p style="margin:20px 0 0;text-align:center;font-size:12px;color:#9CA3AF;line-height:1.6;">
|
||||
Du kannst dein Abo jederzeit unter Einstellungen → Abonnement kündigen.
|
||||
</p>
|
||||
|
||||
@endsection
|
||||
|
|
@ -5,8 +5,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', config('app.name'))</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}" media="(prefers-color-scheme: light)">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon-dark.svg') }}" media="(prefers-color-scheme: dark)">
|
||||
@vite(['resources/css/app.css'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
|
|
@ -60,7 +58,7 @@
|
|||
const STORAGE_KEY = 'aziros_seen_version';
|
||||
const seenVersion = localStorage.getItem(STORAGE_KEY);
|
||||
try {
|
||||
const res = await fetch('{{ config("app.api_url") }}/v1/version/current?platform=web');
|
||||
const res = await fetch('/api/v1/version/current?platform=web');
|
||||
const data = await res.json();
|
||||
const current = data.data;
|
||||
if (!current) return;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>{{ config('app.name') }}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}" media="(prefers-color-scheme: light)">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('favicon-dark.svg') }}" media="(prefers-color-scheme: dark)">
|
||||
|
||||
<meta property="og:title" content="Aziros — KI-Assistent für deinen Alltag"/>
|
||||
<meta property="og:description" content="Termine, Aufgaben und Aria — jetzt auch als App für iOS und Android."/>
|
||||
|
|
|
|||
|
|
@ -69,74 +69,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Webhook --}}
|
||||
@php
|
||||
$lastTrigger = \Illuminate\Support\Facades\Cache::get('last_deploy_triggered_at');
|
||||
$git = $this->gitInfo;
|
||||
@endphp
|
||||
<div class="bg-white rounded-2xl border border-gray-100 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-gray-900">Deploy Webhook</h3>
|
||||
@if($deployStatus === 'success')
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-green-700 bg-green-50 px-2.5 py-1 rounded-full">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-500 inline-block"></span>
|
||||
Erfolgreich ausgelöst
|
||||
</span>
|
||||
@elseif($deployStatus === 'error')
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-red-700 bg-red-50 px-2.5 py-1 rounded-full">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-red-500 inline-block"></span>
|
||||
Fehler
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
|
||||
<div class="bg-gray-50 rounded-xl px-4 py-3">
|
||||
<p class="text-xs font-medium text-gray-500 mb-1">Letzter Commit</p>
|
||||
<p class="font-mono text-sm font-semibold text-gray-900">{{ $git['hash'] }}</p>
|
||||
<p class="text-xs text-gray-600 mt-0.5 truncate" title="{{ $git['message'] }}">{{ $git['message'] }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl px-4 py-3">
|
||||
<p class="text-xs font-medium text-gray-500 mb-1">Commit-Datum</p>
|
||||
<p class="text-sm text-gray-800">{{ $git['date'] !== '—' ? \Carbon\Carbon::parse($git['date'])->format('d.m.Y H:i') : '—' }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl px-4 py-3">
|
||||
<p class="text-xs font-medium text-gray-500 mb-1">Letzter Deploy</p>
|
||||
<p class="text-sm text-gray-800">
|
||||
{{ $lastTrigger ? \Carbon\Carbon::parse($lastTrigger)->format('d.m.Y H:i') : '—' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($deployStatus === 'error' && $deployMessage)
|
||||
<div class="mb-4 bg-red-50 border border-red-100 rounded-xl px-4 py-2 text-xs text-red-700 font-mono">
|
||||
{{ $deployMessage }}
|
||||
</div>
|
||||
@elseif($deployStatus === 'success' && $deployMessage)
|
||||
<div class="mb-4 bg-green-50 border border-green-100 rounded-xl px-4 py-2 text-xs text-green-700">
|
||||
{{ $deployMessage }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button wire:click="triggerDeploy"
|
||||
wire:loading.attr="disabled"
|
||||
class="bg-indigo-600 text-white rounded-xl px-4 py-2 text-sm font-medium hover:bg-indigo-700 disabled:opacity-60 flex items-center gap-2">
|
||||
<span wire:loading.remove wire:target="triggerDeploy">
|
||||
<svg class="w-4 h-4 inline -mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
Deploy auslösen
|
||||
</span>
|
||||
<span wire:loading wire:target="triggerDeploy" class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
||||
</svg>
|
||||
Wird ausgelöst…
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Liste --}}
|
||||
<div class="bg-white rounded-2xl border border-gray-100 overflow-hidden">
|
||||
<table class="w-full">
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@
|
|||
@else {{ $r['minutes'] ?? 30 }}{{ t('events.reminder_min_before') }}
|
||||
@endif
|
||||
@elseif(($r['type'] ?? '') === 'time_of_day')
|
||||
{{ t('events.reminder_on_day_prefix') }}{{ isset($r['time']) ? \Carbon\Carbon::createFromFormat('H:i', $r['time'], 'UTC')->setTimezone(auth()->user()->timezone ?? 'UTC')->format('H:i') : '' }}{{ t('events.reminder_clock_suffix') }}
|
||||
{{ t('events.reminder_on_day_prefix') }}{{ $r['time'] ?? '' }}{{ t('events.reminder_clock_suffix') }}
|
||||
@else
|
||||
{{ t('events.reminder_prev_day_prefix') }}{{ isset($r['time']) ? \Carbon\Carbon::createFromFormat('H:i', $r['time'], 'UTC')->setTimezone(auth()->user()->timezone ?? 'UTC')->format('H:i') : '' }}{{ t('events.reminder_clock_suffix') }}
|
||||
{{ t('events.reminder_prev_day_prefix') }}{{ $r['time'] ?? '' }}{{ t('events.reminder_clock_suffix') }}
|
||||
@endif
|
||||
</span>
|
||||
<button wire:click="removeReminder({{ $i }})" type="button"
|
||||
|
|
|
|||
|
|
@ -26,15 +26,7 @@
|
|||
x-on:dragleave="onDragLeave($event)"
|
||||
x-on:drop="onDrop($event, '{{ $dayStr }}')"
|
||||
x-on:dblclick.self="onCreateAt($event, '{{ $dayStr }}')"
|
||||
x-on:click.self="focusedEventId = null"
|
||||
x-on:mousemove="onTimeHover($event, {{ $colIndex }})"
|
||||
x-on:mouseleave="onTimeLeave($event, {{ $colIndex }})">
|
||||
|
||||
{{-- Hover-Highlight --}}
|
||||
<div class="absolute left-0 right-0 pointer-events-none"
|
||||
x-show="hoverCol === {{ $colIndex }} ? hoverSlot !== null : false"
|
||||
:style="{ top: (hoverSlot * {{ $cellPx / 4 }}) + 'px' }"
|
||||
style="height:{{ $cellPx / 4 }}px;background:rgba(99,102,241,0.07);z-index:1;"></div>
|
||||
x-on:click.self="focusedEventId = null">
|
||||
|
||||
{{-- Stunden-Linien --}}
|
||||
@for($h = $hourStart; $h < $hourEnd; $h++)
|
||||
|
|
|
|||
|
|
@ -256,38 +256,6 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Widerrufsrecht-Checkboxen (nur bei paid plans) --}}
|
||||
@if($this->checkoutType !== 'cancel')
|
||||
<div class="space-y-3 border border-amber-100 bg-amber-50 rounded-xl p-4">
|
||||
<div class="flex items-start gap-2.5">
|
||||
<x-heroicon-o-information-circle class="w-4 h-4 text-amber-500 shrink-0 mt-0.5"/>
|
||||
<p class="text-[11px] text-amber-700 font-medium leading-relaxed">
|
||||
{{ t('checkout.waiver_info') }}
|
||||
</p>
|
||||
</div>
|
||||
<label class="flex items-start gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model.live="rightAcknowledged"
|
||||
class="mt-0.5 rounded border-amber-300 text-amber-500 focus:ring-amber-400 shrink-0"
|
||||
/>
|
||||
<span class="text-[11px] text-amber-800 leading-relaxed">
|
||||
{{ t('checkout.waiver_right_acknowledged') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model.live="waiverConfirmed"
|
||||
class="mt-0.5 rounded border-amber-300 text-amber-500 focus:ring-amber-400 shrink-0"
|
||||
/>
|
||||
<span class="text-[11px] text-amber-800 leading-relaxed">
|
||||
{{ t('checkout.waiver_confirmed') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- CTA --}}
|
||||
@if($this->checkoutType === 'cancel')
|
||||
<button
|
||||
|
|
@ -310,7 +278,6 @@
|
|||
<button
|
||||
wire:click="startCheckout"
|
||||
wire:loading.attr="disabled"
|
||||
:disabled="!$wire.rightAcknowledged || !$wire.waiverConfirmed"
|
||||
class="w-full flex items-center justify-center gap-2 px-5 py-3.5 rounded-xl bg-indigo-600 text-white font-semibold hover:bg-indigo-700 active:scale-95 transition-all shadow-sm shadow-indigo-200 disabled:opacity-60">
|
||||
<span wire:loading.remove wire:target="startCheckout">
|
||||
<x-heroicon-o-arrow-path class="w-4 h-4 inline-block mr-1 opacity-70"/>
|
||||
|
|
@ -328,7 +295,6 @@
|
|||
<button
|
||||
wire:click="startCheckout"
|
||||
wire:loading.attr="disabled"
|
||||
:disabled="!$wire.rightAcknowledged || !$wire.waiverConfirmed"
|
||||
class="w-full flex items-center justify-center gap-2 px-5 py-3.5 rounded-xl bg-indigo-600 text-white font-semibold hover:bg-indigo-700 active:scale-95 transition-all shadow-sm shadow-indigo-200 disabled:opacity-60">
|
||||
<span wire:loading.remove wire:target="startCheckout">
|
||||
<x-heroicon-o-lock-closed class="w-4 h-4 inline-block mr-1 opacity-70"/>
|
||||
|
|
|
|||
|
|
@ -263,10 +263,10 @@
|
|||
@endphp
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
@if($isOverdue)
|
||||
<span class="inline-flex items-center gap-1 text-xs text-red-500 font-medium bg-red-50 px-1.5 py-0.5 rounded-md">
|
||||
<x-heroicon-o-exclamation-triangle class="w-3.5 h-3.5 shrink-0"/>
|
||||
{{ t('dashboard.overdue') }} · {{ $task->due_at->format('d.m.') }}
|
||||
<span class="text-xs text-red-500 font-medium">
|
||||
{{ $task->due_at->format('d.m.') }}
|
||||
</span>
|
||||
<x-heroicon-o-exclamation-triangle class="w-3.5 h-3.5 text-red-400"/>
|
||||
@elseif($task->due_at)
|
||||
<span class="text-[11px] text-gray-400 whitespace-nowrap">
|
||||
{{ $task->due_at->format('d.m.') }}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
'smtp' => ['label' => t('settings.tab.smtp'), 'icon' => 'envelope'],
|
||||
'credits' => ['label' => t('settings.tab.credits'), 'icon' => 'bolt'],
|
||||
...(!auth()->user()->isInternalUser() ? ['affiliate' => ['label' => 'Affiliate', 'icon' => 'gift']] : []),
|
||||
'api' => ['label' => 'API', 'icon' => 'code-bracket'],
|
||||
'account' => ['label' => t('settings.tab.account'), 'icon' => 'shield-exclamation'],
|
||||
] as $key => $tab)
|
||||
<button wire:click="$set('activeTab', '{{ $key }}')"
|
||||
|
|
@ -32,7 +31,6 @@
|
|||
@elseif($tab['icon'] === 'envelope') <x-heroicon-o-envelope class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'bolt') <x-heroicon-o-bolt class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'gift') <x-heroicon-o-gift class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'code-bracket') <x-heroicon-o-code-bracket class="w-3.5 h-3.5"/>
|
||||
@elseif($tab['icon'] === 'shield-exclamation') <x-heroicon-o-shield-exclamation class="w-3.5 h-3.5"/>
|
||||
@endif
|
||||
{{ $tab['label'] }}
|
||||
|
|
@ -660,110 +658,6 @@
|
|||
@endif
|
||||
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
{{-- TAB: API --}}
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
<div x-show="$wire.activeTab === 'api'" x-cloak>
|
||||
<div class="bg-white border border-gray-100 rounded-xl shadow-sm">
|
||||
|
||||
<div class="flex items-center gap-3 px-6 py-5 border-b border-gray-100">
|
||||
<div class="w-9 h-9 rounded-lg bg-indigo-50 flex items-center justify-center">
|
||||
<x-heroicon-o-code-bracket class="w-4 h-4 text-indigo-500"/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-800">API-Zugang</h3>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Erstelle persönliche Tokens für den Zugriff auf die Aziros-API.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
|
||||
{{-- Neuen Token erstellen --}}
|
||||
@if(auth()->user()->hasFeature('api_access'))
|
||||
<div>
|
||||
<label class="{{ $labelClass }}">Neuen Token erstellen</label>
|
||||
<div class="flex gap-2">
|
||||
<input wire:model="newTokenName"
|
||||
placeholder="z.B. Mein Skript, Zapier…"
|
||||
class="{{ $inputClass }} flex-1"
|
||||
wire:keydown.enter="createApiToken"/>
|
||||
<button wire:click="createApiToken"
|
||||
class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors shrink-0">
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
@error('newTokenName') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
@if($createdToken)
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4 space-y-2">
|
||||
<p class="text-xs font-medium text-green-800">Token wurde erstellt — kopiere ihn jetzt, er wird nur einmal angezeigt.</p>
|
||||
<div class="flex gap-2 items-center" x-data="{ copied: false }">
|
||||
<code class="flex-1 text-xs bg-white border border-green-200 rounded px-3 py-2 text-gray-800 break-all font-mono select-all">{{ $createdToken }}</code>
|
||||
<button @click="navigator.clipboard.writeText('{{ $createdToken }}'); copied = true; setTimeout(() => copied = false, 2000)"
|
||||
class="px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 shrink-0">
|
||||
<span x-show="!copied">Kopieren</span>
|
||||
<span x-show="copied" x-cloak>✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="flex items-center gap-3 p-4 bg-indigo-50 rounded-lg">
|
||||
<x-heroicon-o-lock-closed class="w-5 h-5 text-indigo-400 shrink-0"/>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-indigo-800">Pro-Feature</p>
|
||||
<p class="text-xs text-indigo-600 mt-0.5">API-Tokens sind für Pro-Nutzer verfügbar.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Bestehende Tokens --}}
|
||||
@if($this->apiTokens->isNotEmpty())
|
||||
<div>
|
||||
<label class="{{ $labelClass }}">Aktive Tokens</label>
|
||||
<div class="border border-gray-100 rounded-lg overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-100 text-left text-xs text-gray-500 uppercase tracking-wider">
|
||||
<th class="px-4 py-2.5">Name</th>
|
||||
<th class="px-4 py-2.5">Erstellt</th>
|
||||
<th class="px-4 py-2.5">Zuletzt verwendet</th>
|
||||
<th class="px-4 py-2.5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@foreach($this->apiTokens as $token)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $token->name ?? 'Unbenannt' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">{{ $token->created_at->format('d.m.Y') }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">{{ $token->last_used_at?->diffForHumans() ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<button wire:click="revokeApiToken('{{ $token->id }}')"
|
||||
wire:confirm="Token widerrufen?"
|
||||
class="text-xs text-red-500 hover:text-red-700 font-medium">
|
||||
Widerrufen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- API-Referenz --}}
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
|
||||
<p class="text-xs font-medium text-gray-700">Verwendung</p>
|
||||
<code class="block text-xs text-gray-600 font-mono">Authorization: Bearer <dein-token></code>
|
||||
<code class="block text-xs text-gray-500 font-mono">GET https://api.aziros.com/v1/events?from=2026-01-01&to=2026-12-31</code>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
{{-- TAB: GEFAHRENZONE --}}
|
||||
{{-- ════════════════════════════════════════════════════════════ --}}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mail Preview · Aziros</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body { margin: 0; background: #F4F6FB; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #111827; }
|
||||
.wrapper { max-width: 960px; margin: 0 auto; padding: 48px 24px; }
|
||||
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 36px; }
|
||||
.logo { font-size: 18px; font-weight: 300; color: #111827; letter-spacing: -0.5px; text-decoration: none; }
|
||||
.badge { font-size: 11px; background: #EEF2FF; color: #4F46E5; padding: 3px 10px; border-radius: 20px; font-weight: 600; letter-spacing: 0.3px; }
|
||||
h1 { margin: 0 0 4px; font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
|
||||
.subtitle { margin: 0 0 32px; font-size: 14px; color: #6B7280; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||||
.card { background: #fff; border: 1px solid #E5E7EB; border-radius: 14px; overflow: hidden; transition: box-shadow 0.15s, border-color 0.15s; text-decoration: none; color: inherit; display: block; }
|
||||
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); border-color: #C7D2FE; }
|
||||
.preview-frame { width: 100%; height: 200px; border: none; background: #F9FAFB; pointer-events: none; display: block; }
|
||||
.card-body { padding: 14px 16px; border-top: 1px solid #F3F4F6; }
|
||||
.card-name { font-size: 13px; font-weight: 600; color: #111827; margin: 0 0 2px; }
|
||||
.card-path { font-size: 11px; color: #9CA3AF; margin: 0; font-family: 'SF Mono', Menlo, monospace; }
|
||||
.card-footer { padding: 10px 16px; background: #F9FAFB; border-top: 1px solid #F3F4F6; display: flex; justify-content: flex-end; }
|
||||
.btn { font-size: 12px; color: #4F46E5; font-weight: 600; text-decoration: none; padding: 5px 12px; background: #EEF2FF; border-radius: 6px; }
|
||||
.btn:hover { background: #E0E7FF; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
|
||||
<div class="header">
|
||||
<a href="{{ config('app.url') }}" class="logo">aziros</a>
|
||||
<span class="badge">Mail Preview</span>
|
||||
</div>
|
||||
|
||||
<h1>E-Mail Templates</h1>
|
||||
<p class="subtitle">{{ count($templates) }} Templates — Klicke auf eine Karte für die Vollansicht.</p>
|
||||
|
||||
<div class="grid">
|
||||
@foreach($templates as $tpl)
|
||||
<a href="{{ url('/admin/mail-preview/' . $tpl['key']) }}" class="card" target="_blank">
|
||||
<iframe
|
||||
src="{{ url('/admin/mail-preview/' . $tpl['key']) }}"
|
||||
class="preview-frame"
|
||||
title="{{ $tpl['name'] }}"
|
||||
loading="lazy"
|
||||
style="transform-origin: top left; transform: scale(0.5); width: 200%; height: 400px; margin-bottom: -200px;">
|
||||
</iframe>
|
||||
<div class="card-body">
|
||||
<p class="card-name">{{ $tpl['name'] }}</p>
|
||||
<p class="card-path">emails/{{ str_replace('.', '/', $tpl['key']) }}.blade.php</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="btn">Vollansicht →</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
<div class="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
|
||||
{{-- Logo --}}
|
||||
<a href="{{ route('homepage.index') }}">
|
||||
<x-logo class="h-8 w-auto"/>
|
||||
<a href="{{ route('homepage.index') }}" class="flex items-center gap-2.5">
|
||||
<div class="w-8 h-8 rounded-xl bg-indigo-600 flex items-center justify-center shadow-sm shadow-indigo-200">
|
||||
<x-heroicon-o-bolt class="w-4 h-4 text-white"/>
|
||||
</div>
|
||||
<span class="font-bold text-gray-900 text-lg tracking-tight">aziros</span>
|
||||
</a>
|
||||
|
||||
{{-- Links (desktop) --}}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ Route::prefix('v1')->group(function () {
|
|||
// Kalender
|
||||
Route::get('/events', [EventController::class, 'index']);
|
||||
Route::post('/events', [EventController::class, 'store']);
|
||||
Route::get('/events/{id}', [EventController::class, 'show']);
|
||||
Route::put('/events/{id}', [EventController::class, 'update']);
|
||||
Route::delete('/events/{id}', [EventController::class, 'destroy']);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,13 @@ use Illuminate\Support\Facades\Route;
|
|||
|
||||
|
||||
|
||||
// ── Mail Preview (nur Admin) ──────────────────────────────────────────────────
|
||||
Route::get('/mail-preview', function () {
|
||||
return view('emails.auth.verify', [
|
||||
'user' => 'Max Mustermann',
|
||||
'code' => '123456',
|
||||
'url' => 'https://example.com/verify' // 🔥 hinzufügen
|
||||
]);
|
||||
});
|
||||
|
||||
Route::middleware(['auth.custom'])->group(function () {
|
||||
// intentionally empty — kept for middleware reference
|
||||
|
|
@ -57,97 +63,6 @@ Route::middleware(['user', 'role:admin'])->prefix('admin')->name('admin.')->grou
|
|||
Route::get('/affiliates', \App\Livewire\Admin\Affiliates\Index::class)->name('affiliates.index');
|
||||
Route::get('/translations', TranslationIndex::class)->name('translations.index');
|
||||
Route::get('/versions', \App\Livewire\Admin\Versions::class)->name('versions.index');
|
||||
|
||||
// ── Mail Preview ──────────────────────────────────────────────────────────
|
||||
$fakeData = [
|
||||
'auth.verify' => [
|
||||
'name' => 'E-Mail verifizieren',
|
||||
'data' => ['url' => 'https://app.aziros.com/verify?token=preview', 'user' => 'Max Mustermann'],
|
||||
],
|
||||
'reset-password' => [
|
||||
'name' => 'Passwort zurücksetzen',
|
||||
'data' => ['url' => 'https://app.aziros.com/password/reset/preview', 'user' => (object)['name' => 'Max Mustermann']],
|
||||
],
|
||||
'agent.reminder' => [
|
||||
'name' => 'Terminerinnerung',
|
||||
'data' => [
|
||||
'sender_name' => 'Maria Muster',
|
||||
'event_title' => 'Strategie-Meeting Q2',
|
||||
'event_date' => 'Montag, 22. April 2026',
|
||||
'event_time' => '14:00',
|
||||
'event_end' => '15:30',
|
||||
'event_notes' => 'Bitte die Präsentation vorbereiten.',
|
||||
'recipient_name' => 'Max Mustermann',
|
||||
'message' => 'Vergiss bitte nicht, die Unterlagen mitzubringen!',
|
||||
],
|
||||
],
|
||||
'agent.message' => [
|
||||
'name' => 'Neue Nachricht (Agent)',
|
||||
'data' => [
|
||||
'sender_name' => 'Maria Muster',
|
||||
'recipient_name' => 'Max Mustermann',
|
||||
'message' => 'Hallo Max, kannst du mir bitte die Unterlagen für das Meeting zukommen lassen? Ich brauche sie bis morgen früh.',
|
||||
],
|
||||
],
|
||||
'gift-access' => [
|
||||
'name' => 'Geschenk-Zugang',
|
||||
'data' => [
|
||||
'user' => (object)['name' => 'Max Mustermann'],
|
||||
'plan' => (object)['name' => 'Pro'],
|
||||
'durationLabel' => '3 Monate kostenlos',
|
||||
'endsAt' => \Carbon\Carbon::now()->addMonths(3),
|
||||
],
|
||||
],
|
||||
'affiliate-qualified' => [
|
||||
'name' => 'Affiliate qualifiziert',
|
||||
'data' => [
|
||||
'credits' => 50,
|
||||
'referredUser' => (object)['name' => 'Lisa Müller'],
|
||||
],
|
||||
],
|
||||
'smtp-test' => [
|
||||
'name' => 'SMTP Test',
|
||||
'data' => [],
|
||||
],
|
||||
'aria-composed' => [
|
||||
'name' => 'Aria – Verfasste E-Mail',
|
||||
'data' => ['body' => "Hallo!\n\nIch habe deine Aufgabe erledigt und hier ist die Zusammenfassung der Ergebnisse für dein Meeting morgen.\n\nBitte prüfe die Agenda und gib mir Bescheid, ob noch Änderungen nötig sind."],
|
||||
],
|
||||
'auth.welcome' => [
|
||||
'name' => 'Willkommen',
|
||||
'data' => ['user' => (object)['name' => 'Max Mustermann']],
|
||||
],
|
||||
'subscription.confirmed' => [
|
||||
'name' => 'Abo bestätigt',
|
||||
'data' => [
|
||||
'user' => (object)['name' => 'Max Mustermann'],
|
||||
'plan' => 'Pro',
|
||||
'billing' => 'monthly',
|
||||
'amount' => 1900,
|
||||
'renewsAt' => \Carbon\Carbon::now()->addMonth(),
|
||||
],
|
||||
],
|
||||
'subscription.cancelled' => [
|
||||
'name' => 'Abo gekündigt',
|
||||
'data' => [
|
||||
'user' => (object)['name' => 'Max Mustermann'],
|
||||
'plan' => 'Pro',
|
||||
'endsAt' => \Carbon\Carbon::now()->addDays(14),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
Route::get('/mail-preview', function () use ($fakeData) {
|
||||
$templates = collect($fakeData)->map(fn($v, $k) => ['key' => $k, 'name' => $v['name']])->values();
|
||||
return view('mail-preview.index', compact('templates'));
|
||||
})->name('mail-preview.index');
|
||||
|
||||
Route::get('/mail-preview/{template}', function (string $template) use ($fakeData) {
|
||||
abort_unless(isset($fakeData[$template]), 404);
|
||||
$entry = $fakeData[$template];
|
||||
$view = 'emails.' . $template;
|
||||
return response(view($view, $entry['data']));
|
||||
})->name('mail-preview.show');
|
||||
});
|
||||
|
||||
Route::middleware('user')->group(function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue