Fix: Solucionados problemas de permisos en Docker y agregada gestion de Webhook de Telegram

This commit is contained in:
2026-04-19 19:23:25 -06:00
parent 249c997257
commit c4bd4b62e3
13 changed files with 615 additions and 98 deletions

45
.env.docker Normal file
View File

@@ -0,0 +1,45 @@
# Variables de entorno para Docker
# Copiar a .env y completar los valores faltantes
APP_NAME="Nomina Ventas"
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_URL=http://localhost:8004
APP_LOCALE=es
APP_FALLBACK_LOCALE=es
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_LEVEL=debug
# Base de datos REMOTA
DB_CONNECTION=mysql
DB_HOST=10.10.4.17
DB_PORT=3391
DB_DATABASE=nomina_pegaso
DB_USERNAME=nickpons666
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# Telegram Bot (opcional)
TELEGRAM_BOT_TOKEN=
TELEGRAM_WEBHOOK_URL=
# Docker Runtime
DATA_DIR=/media/DATOS/AppData/nomina_pegaso
APP_PORT=8004
RUN_MIGRATIONS=false

View File

@@ -1,26 +1,15 @@
# ============================================
# Laravel PHP-FPM 8.3 - Production Optimized
# Laravel - Single Image (Nginx + PHP-FPM)
# ============================================
# Stage 1: Builder with Composer
FROM composer:2 AS composer-builder
WORKDIR /app
# Copy composer files first for better layer caching
COPY composer.json composer.lock* ./
RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
# Stage 2: PHP-FPM Production Image
FROM php:8.3-fpm-bookworm
# Build arguments for dynamic user UID/GID
ARG PUID=1000
ARG PGID=1000
# Labels
LABEL maintainer="dev@local.dev" \
description="Laravel PHP-FPM 8.3 production container"
description="Laravel single container with Nginx"
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -37,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libsqlite3-dev \
unzip \
git \
nginx \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
@@ -59,48 +49,43 @@ RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.
&& echo "opcache.revalidate_freq=2" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
&& echo "opcache.fast_shutdown=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
&& echo "upload_max_filesize=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
&& echo "post_max_size=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
&& echo "post_max_size=100M" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
&& echo "listen = 0.0.0.0:9000" >> /usr/local/etc/php-fpm.d/www.conf \
&& sed -i 's/user = www-data/user = laravel/g' /usr/local/etc/php-fpm.d/www.conf \
&& sed -i 's/group = www-data/group = laravel/g' /usr/local/etc/php-fpm.d/www.conf \
&& echo "sys_temp_dir = /tmp" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
&& echo "upload_tmp_dir = /tmp" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
# Copy Composer from builder stage
COPY --from=composer-builder /usr/bin/composer /usr/bin/composer
COPY --from=composer-builder /root/.composer /root/.composer
# Create Laravel storage directories with proper permissions
# Create Laravel storage directories
RUN mkdir -p /var/www/html/storage/framework/{cache,sessions,views} \
&& mkdir -p /var/www/html/storage/logs \
&& mkdir -p /var/www/html/bootstrap/cache \
&& chmod -R 775 /var/www/html/storage \
&& chmod -R 775 /var/www/html/bootstrap/cache
# Create system user for Laravel (dynamic UID/GID)
# Create system user for Laravel
RUN groupadd -g ${PGID} laravel && \
useradd -u ${PUID} -g laravel -m -s /bin/bash laravel
# Set working directory
# Copy nginx config
COPY docker/nginx/nginx.conf /etc/nginx/sites-available/default
WORKDIR /var/www/html
# Copy application files
COPY --chown=laravel:laravel . /var/www/html
# Fix storage permissions
# Fix permissions
RUN chown -R laravel:laravel /var/www/html/storage \
&& chown -R laravel:laravel /var/www/html/bootstrap/cache \
&& chmod -R 775 /var/www/html/storage \
&& chmod -R 775 /var/www/html/bootstrap/cache
# Switch to non-root user
USER laravel
# Copy entrypoint
COPY --chown=root:root docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose port 9000 (PHP-FPM)
EXPOSE 9000
# Expose port 80 (interno)
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD php-fpm-healthcheck || exit 1
# Create healthcheck script
RUN echo '#!/bin/sh' > /usr/local/bin/php-fpm-healthcheck \
&& echo 'SCRIPT_NAME=/health.php SCRIPT_FILENAME=/health.php REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 > /dev/null 2>&1' >> /usr/local/bin/php-fpm-healthcheck \
&& chmod +x /usr/local/bin/php-fpm-healthcheck
CMD ["php-fpm"]
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -37,7 +37,10 @@ class TelegramController extends Controller
$telegramAccount->update(['verification_code' => $verificationCode]);
}
return view('telegram.verify', compact('telegramAccount'));
return view('telegram.verify', [
'telegramAccount' => $telegramAccount,
'webhookInfo' => (new TelegramBotService())->getWebhookInfo()
]);
}
/**
@@ -98,8 +101,7 @@ class TelegramController extends Controller
Log::info('Telegram webhook received', $update);
if (empty($update)) {
return response()->json(['ok' => true, 'message' => 'No update
received']);
return response()->json(['ok' => true, 'message' => 'No update received']);
}
$botService = new TelegramBotService();
@@ -117,24 +119,32 @@ received']);
}
/**
* Configurar webhook (ruta temporal para configurar desde el navegador)
* Configurar webhook
*/
public function setupWebhook(Request $request)
{
$token = $request->get('token');
// Verificar token de seguridad simple
if ($token !== config('app.key')) {
abort(403, 'Token inválido');
}
$botService = new TelegramBotService();
$result = $botService->setWebhook();
if ($result) {
return response()->json(['success' => true, 'message' => 'Webhook configurado correctamente']);
return back()->with('success', 'Webhook configurado correctamente.');
}
return response()->json(['success' => false, 'error' => 'Error al configurar webhook'], 500);
return back()->with('error', 'Error al configurar webhook.');
}
/**
* Borrar webhook
*/
public function deleteWebhook(Request $request)
{
$botService = new TelegramBotService();
$result = $botService->deleteWebhook();
if ($result) {
return back()->with('success', 'Webhook borrado correctamente.');
}
return back()->with('error', 'Error al borrar el webhook.');
}
}

View File

@@ -297,4 +297,40 @@ class TelegramBotService
return false;
}
}
/**
* Obtener información del webhook
*/
public function getWebhookInfo(): array
{
if (!$this->botToken) {
return ['ok' => false, 'error' => 'Bot token not configured'];
}
try {
$response = Http::get("https://api.telegram.org/bot{$this->botToken}/getWebhookInfo");
return $response->json();
} catch (\Exception $e) {
Log::error('Telegram get webhook info error', ['error' => $e->getMessage()]);
return ['ok' => false, 'error' => $e->getMessage()];
}
}
/**
* Borrar webhook
*/
public function deleteWebhook(): bool
{
if (!$this->botToken) {
return false;
}
try {
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/deleteWebhook");
return $response->json('ok', false);
} catch (\Exception $e) {
Log::error('Telegram delete webhook error', ['error' => $e->getMessage()]);
return false;
}
}
}

93
build.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/bin/bash
set -e
REGISTRY="registry-pons.duckdns.org"
IMAGE_NAME="nomina-ventas"
echo "========================================"
echo " Build Docker Image - Nomina Ventas"
echo "========================================"
echo ""
# 1. Ask for tag
read -p "Ingresa el tag de la imagen (ej: latest, v1.0.0): " TAG
if [ -z "$TAG" ]; then
TAG="latest"
fi
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "Imagen: $FULL_IMAGE"
echo ""
# 2. Ask for cache
echo "¿Quieres construir con cache (más rápido) o sin cache (más limpio)?"
select CACHE_CHOICE in "Con cache" "Sin cache (--no-cache)"; do
case $CACHE_CHOICE in
"Con cache") BUILD_CACHE="" ;;
"Sin cache (--no-cache)") BUILD_CACHE="--no-cache" ;;
esac
break
done
echo ""
# 3. Ask for registry push
echo "¿Quieres subir la imagen al registry?"
select PUSH_CHOICE in "Sí" "No (solo construir local)"; do
case $PUSH_CHOICE in
"Sí") DO_PUSH=true ;;
"No (solo construir local)") DO_PUSH=false ;;
esac
break
done
echo ""
# 4. Build the image
echo "========================================"
echo " Construyendo imagen..."
echo "========================================"
docker build \
--tag "${FULL_IMAGE}" \
${BUILD_CACHE} \
--build-arg PUID=$(id -u) \
--build-arg PGID=$(id -g) \
.
echo ""
echo "✅ Imagen construida: ${FULL_IMAGE}"
echo ""
# 5. Push if requested
if [ "$DO_PUSH" = true ]; then
echo "========================================"
echo " Subiendo al registry..."
echo "========================================"
docker push "${FULL_IMAGE}"
echo ""
echo "✅ Imagen subida al registry"
fi
# 6. Update docker-compose.prod.yml with the tag
echo "========================================"
echo " Actualizando docker-compose.prod.yml"
echo "========================================"
sed -i "s|image: \${REGISTRY:-registry-pons.duckdns.org}/\${IMAGE:-nomina-ventas}:\${TAG:-latest}|image: ${FULL_IMAGE}|g" docker-compose.prod.yml
echo "✅ docker-compose.prod.yml actualizado"
echo ""
echo "========================================"
echo " Resumen"
echo "========================================"
echo " Imagen local: ${FULL_IMAGE}"
[ "$DO_PUSH" = true ] && echo " Registry: ${FULL_IMAGE}"
echo ""
echo " Para producción:"
echo " 1. Edita .env.docker con tus valores"
echo " 2. cp .env.docker .env"
echo " 3. docker compose -f docker-compose.prod.yml up -d"
echo ""
echo " El puerto expuesto será: 8004"
echo " Los datos se almacenarán en: /media/DATOS/AppData/nomina_pegaso"
echo ""
echo "✅ Completado!"

44
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,44 @@
services:
app:
image: registry-pons.duckdns.org/nomina-ventas:latest
container_name: nomina_app
ports:
- "8004:80"
volumes:
- /media/DATOS/AppData/nomina_pegaso/storage:/var/www/html/storage
- /media/DATOS/AppData/nomina_pegaso/bootstrap/cache:/var/www/html/bootstrap/cache
environment:
- APP_NAME=Nomina Ventas
- APP_ENV=production
- APP_DEBUG=true
- APP_KEY=
- APP_URL=https://nomina-pegaso.duckdns.org
- APP_LOCALE=es
- APP_FALLBACK_LOCALE=es
- BCRYPT_ROUNDS=12
- LOG_CHANNEL=stack
- LOG_LEVEL=debug
- DB_CONNECTION=mysql
- DB_HOST=10.10.4.17
- DB_PORT=3390
- DB_DATABASE=nomina_pegaso
- DB_USERNAME=nickpons666
- DB_PASSWORD=MiPo6425@@
- SESSION_DRIVER=database
- SESSION_LIFETIME=120
- SESSION_ENCRYPT=true
- SESSION_PATH=/
- SESSION_DOMAIN=
- BROADCAST_CONNECTION=log
- FILESYSTEM_DISK=local
- QUEUE_CONNECTION=database
- CACHE_STORE=database
- TELEGRAM_BOT_TOKEN=8324407449:AAF2awMeZ9pgSIp0MvV1r5owu8lO7SEK70E
- TELEGRAM_WEBHOOK_URL=
- RUN_MIGRATIONS=false
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M

View File

@@ -1,28 +1,4 @@
services:
nginx:
image: nginx:alpine
container_name: nomina_nginx
ports:
- "${APP_PORT:-80}:80"
volumes:
- ./public:/var/www/html/public:ro
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- app_network
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
app:
build:
context: .
@@ -31,29 +7,26 @@ services:
PUID: ${PUID:-1000}
PGID: ${PGID:-1000}
container_name: nomina_app
ports:
- "${APP_PORT:-8004}:80"
volumes:
- .:/var/www/html:delegated
environment:
- APP_ENV=${APP_ENV:-production}
- APP_DEBUG=${APP_DEBUG:-false}
- APP_ENV=${APP_ENV:-local}
- APP_DEBUG=${APP_DEBUG:-true}
- APP_KEY=${APP_KEY}
- APP_URL=${APP_URL:-http://localhost}
- APP_URL=${APP_URL:-http://localhost:8004}
- DB_CONNECTION=${DB_CONNECTION:-mysql}
- DB_HOST=${DB_HOST:-db}
- DB_PORT=${DB_PORT:-3306}
- DB_DATABASE=${DB_DATABASE:-nomina_ventas}
- DB_USERNAME=${DB_USERNAME}
- DB_HOST=${DB_HOST:-10.10.4.17}
- DB_PORT=${DB_PORT:-3391}
- DB_DATABASE=${DB_DATABASE:-nomina_pegaso}
- DB_USERNAME=${DB_USERNAME:-nickpons666}
- DB_PASSWORD=${DB_PASSWORD}
- SESSION_DRIVER=${SESSION_DRIVER:-database}
- CACHE_STORE=${CACHE_STORE:-database}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL}
volumes:
- ./:/var/www/html:delegated
- storage_data:/var/www/html/storage
depends_on:
db:
condition: service_healthy
networks:
- app_network
- RUN_MIGRATIONS=${RUN_MIGRATIONS:-false}
restart: unless-stopped
deploy:
resources:
@@ -66,13 +39,13 @@ services:
container_name: nomina_mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${DB_DATABASE:-nomina_ventas}
- MYSQL_USER=${DB_USERNAME}
- MYSQL_DATABASE=${DB_DATABASE:-nomina_pegaso}
- MYSQL_USER=${DB_USERNAME:-nickpons666}
- MYSQL_PASSWORD=${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
networks:
- app_network
ports:
- "${DB_PORT:-3391}:3306"
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
@@ -87,9 +60,8 @@ services:
memory: 1G
networks:
app_network:
default:
driver: bridge
volumes:
mysql_data:
storage_data:
mysql_data:

100
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,100 @@
#!/bin/bash
set -e
echo "========================================"
echo " Nomina Ventas - Entrypoint"
echo "========================================"
# Verificar que el código existe en /var/www/html
if [ ! -f /var/www/html/artisan ]; then
echo "ERROR: No se encontró el código en /var/www/html"
echo "Montando volumen con el código..."
exit 1
fi
# Asegurar que existan los directorios necesarios (importante para volúmenes montados)
mkdir -p /var/www/html/storage/framework/{cache,sessions,views}
mkdir -p /var/www/html/storage/logs
mkdir -p /var/www/html/bootstrap/cache
# Asegurar permisos
chown -R laravel:laravel /var/www/html/storage
chown -R laravel:laravel /var/www/html/bootstrap/cache
chmod -R 775 /var/www/html/storage
chmod -R 775 /var/www/html/bootstrap/cache
# ===== GENERAR O ACTUALIZAR .env =====
echo "Intentando sincronizar .env desde variables de Docker..."
# Generamos el contenido en un archivo temporal
cat > /tmp/.env.tmp << EOF
APP_NAME="${APP_NAME:-Laravel}"
APP_ENV="${APP_ENV:-production}"
APP_KEY="${APP_KEY}"
APP_DEBUG=${APP_DEBUG:-false}
APP_URL=${APP_URL:-http://localhost}
APP_LOCALE=${APP_LOCALE:-es}
APP_FALLBACK_LOCALE=${APP_FALLBACK_LOCALE:-es}
LOG_CHANNEL=${LOG_CHANNEL:-stack}
LOG_LEVEL=${LOG_LEVEL:-debug}
DB_CONNECTION=${DB_CONNECTION:-mysql}
DB_HOST=${DB_HOST:-127.0.0.1}
DB_PORT=${DB_PORT:-3306}
DB_DATABASE=${DB_DATABASE:-laravel}
DB_USERNAME=${DB_USERNAME:-root}
DB_PASSWORD='${DB_PASSWORD}'
SESSION_DRIVER=${SESSION_DRIVER:-file}
SESSION_LIFETIME=${SESSION_LIFETIME:-120}
SESSION_ENCRYPT=${SESSION_ENCRYPT:-false}
SESSION_PATH=${SESSION_PATH:-/}
SESSION_DOMAIN=${SESSION_DOMAIN:-null}
BROADCAST_CONNECTION=${BROADCAST_CONNECTION:-log}
FILESYSTEM_DISK=${FILESYSTEM_DISK:-local}
QUEUE_CONNECTION=${QUEUE_CONNECTION:-sync}
CACHE_STORE=${CACHE_STORE:-database}
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL}
BCRYPT_ROUNDS=${BCRYPT_ROUNDS:-12}
EOF
# Intentar mover el temporal al destino final
if cp /tmp/.env.tmp /var/www/html/.env 2>/dev/null; then
# Si pudimos copiarlo, aplicamos permisos
chown laravel:laravel /var/www/html/.env
chmod 640 /var/www/html/.env
# Generar APP_KEY si falta (solo si es escribible)
if [ -z "$APP_KEY" ] || [ "$APP_KEY" == "" ]; then
echo "APP_KEY no detectada, generando una nueva..."
NEW_KEY=$(php /var/www/html/artisan key:generate --show --no-ansi)
sed -i "s|APP_KEY=|APP_KEY=$NEW_KEY|g" /var/www/html/.env
export APP_KEY=$NEW_KEY
fi
echo "✅ Archivo .env sincronizado correctamente."
else
echo "⚠️ ADVERTENCIA: No se pudo escribir en /var/www/html/.env"
echo " Probablemente esté montado como volumen de solo lectura (:ro)."
echo " Se usarán los valores del archivo montado externamente."
fi
rm -f /tmp/.env.tmp
# ===== LIMPIAR CACHE =====
echo ">> Limpiando cache..."
php /var/www/html/artisan view:clear 2>/dev/null || true
php /var/www/html/artisan config:clear 2>/dev/null || true
php /var/www/html/artisan cache:clear 2>/dev/null || true
php /var/www/html/artisan route:clear 2>/dev/null || true
echo ">> Cacheando configuración..."
php /var/www/html/artisan config:cache 2>&1 || true
# ===== VERIFICAR =====
php /var/www/html/artisan about 2>&1 | head -10 || true
echo "========================================"
echo " Iniciando servicios..."
echo "========================================"
php-fpm &
nginx -g "daemon off;"

View File

@@ -1,6 +1,6 @@
server {
listen 80;
server_name localhost;
server_name _;
root /var/www/html/public;
index index.php index.html;
@@ -24,7 +24,7 @@ server {
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;

159
prompt.txt Normal file
View File

@@ -0,0 +1,159 @@
Crea una aplicación web para el control de ingresos por comisiones y gastos personales con soporte multiusuario y un bot de Telegram integrado.
## Descripción general
La aplicación debe permitir a múltiples usuarios registrar ventas diarias por mes mediante un calendario interactivo, calcular comisiones basadas en un porcentaje configurable, sumar un sueldo mensual fijo, registrar gastos y mostrar el dinero restante tanto mensual como por quincena.
Cada usuario debe tener acceso únicamente a su propia información.
## Funcionalidades principales
### 1. Sistema multiusuario
* Registro e inicio de sesión
* Cada usuario solo puede ver y modificar sus propios datos
* Relación de todos los datos con user_id
### 2. Gestión por mes
* Crear, ver y editar meses por usuario
### 3. Calendario de captura (IMPORTANTE)
* Cada mes debe mostrarse como un calendario visual
* El usuario NO captura la fecha manualmente
* El usuario selecciona un día haciendo clic en el calendario
Al seleccionar un día:
* Se abre un formulario/modal para capturar:
* Ventas registradas por el usuario
* Ventas del sistema de la empresa
El sistema debe:
* Guardar automáticamente la fecha según el día seleccionado
* Mostrar visualmente en el calendario:
* Días con información registrada
* Días sin registrar
* Indicadores rápidos (ej: colores o íconos)
### 4. Registro diario de ventas
Por cada día seleccionado:
* Ventas registradas por el usuario
* Ventas del sistema de la empresa
Cálculos:
* Diferencia entre valores
* Comisión diaria
### 5. Configuración de variables por usuario
Cada usuario puede definir:
* Porcentaje de comisión
* Sueldo mensual
### 6. Cálculos automáticos
* Comisión diaria = ventas del sistema × porcentaje
* Total mensual de comisiones
* Total ingresos = sueldo + comisiones
* Total gastos
* Balance final
### 7. Gestión de gastos
* Agregar múltiples gastos:
* Descripción
* Monto
* Fecha (opcional o seleccionable desde calendario)
### 8. Cálculo por quincena
* Primera quincena (115)
* Segunda quincena (16fin)
Mostrar:
* Ingresos
* Gastos
* Balance
### 9. Dashboard
* Resumen mensual
* Totales
* Vista de calendario
* Tabla opcional de ventas
* Resumen por quincena
## Integración con Telegram
### Vinculación segura
* El usuario inicia sesión en la web
* Genera un código único de vinculación
* En Telegram, el usuario envía ese código al bot
* El bot vincula el chat_id con el usuario
### Funcionalidades del bot
El usuario puede:
* Registrar ventas:
comando: /venta 1500 1400
(el sistema usa automáticamente la fecha actual)
* Registrar gastos:
comando: /gasto comida 200
* Consultar resumen:
comando: /resumen
* Consultar quincena:
comando: /quincena
### Seguridad
* No pedir usuario/contraseña en Telegram
* Validar chat_id vinculado
* Solo permitir acceso a datos del usuario vinculado
## Base de datos (sugerida)
* users (id, name, email, password)
* telegram_accounts (id, user_id, chat_id)
* months (id, user_id, name, year)
* daily_sales (id, user_id, month_id, date, user_sales, system_sales)
* settings (id, user_id, commission_percentage, monthly_salary)
* expenses (id, user_id, month_id, description, amount, date)
## Requisitos técnicos
* Backend: Laravel
* Frontend: Blade o Vue
* Base de datos: MySQL
* Uso de librería de calendario (ej: FullCalendar)
## Extras opcionales
* Exportar a Excel/CSV
* Gráficas
* Notificaciones por Telegram
## UI/UX
* Interfaz tipo dashboard
* Calendario como elemento principal
* Colores para indicar estado de días
* Captura rápida mediante modales
La aplicación debe ser rápida, intuitiva y enfocada en facilitar la captura diaria sin errores y todos los logs se deben de guardar en la carpeta logs de la raiz.

View File

@@ -60,7 +60,7 @@
</div>
<div class="col-md-4">
<div class="card">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Información</h5>
</div>
@@ -73,6 +73,64 @@
</ul>
</div>
</div>
<div class="card shadow-sm border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-robot"></i> Gestión Webhook</h5>
</div>
<div class="card-body">
@if(isset($webhookInfo['ok']) && $webhookInfo['ok'])
@php $info = $webhookInfo['result']; @endphp
<div class="mb-3">
<label class="small fw-bold text-muted d-block">Estado:</label>
@if(!empty($info['url']))
<span class="badge bg-success">Configurado</span>
@else
<span class="badge bg-warning text-dark">No configurado</span>
@endif
</div>
@if(!empty($info['url']))
<div class="mb-3">
<label class="small fw-bold text-muted d-block">URL Webhook:</label>
<code class="small break-all" style="word-break: break-all;">{{ $info['url'] }}</code>
</div>
<div class="mb-3 small">
<label class="small fw-bold text-muted d-block">Pendientes:</label>
{{ $info['pending_update_count'] ?? 0 }} mensajes
</div>
@endif
<div class="d-grid gap-2">
<form action="{{ route('telegram.setup-webhook') }}" method="POST">
@csrf
<button type="submit" class="btn btn-sm btn-outline-primary w-100">
<i class="bi bi-gear-fill"></i> {{ empty($info['url']) ? 'Configurar Webhook' : 'Actualizar Webhook' }}
</button>
</form>
@if(!empty($info['url']))
<form action="{{ route('telegram.delete-webhook') }}" method="POST">
@csrf
<button type="submit" class="btn btn-sm btn-outline-danger w-100" onclick="return confirm('¿Seguro que deseas borrar el webhook?')">
<i class="bi bi-trash-fill"></i> Borrar Webhook
</button>
</form>
@endif
</div>
@else
<div class="alert alert-danger small">
<i class="bi bi-exclamation-triangle"></i> Error al conectar con Telegram API.
@if(isset($webhookInfo['description']))
<br><small>{{ $webhookInfo['description'] }}</small>
@endif
</div>
@endif
</div>
<div class="card-footer bg-light">
<small class="text-muted italic">Basado en APP_URL: {{ config('app.url') }}</small>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -60,6 +60,8 @@ Route::middleware(['auth'])->group(function () {
Route::get('/telegram/verify', [TelegramController::class, 'showVerifyPage'])->name('telegram.verify');
Route::post('/telegram/regenerate', [TelegramController::class, 'regenerateCode'])->name('telegram.regenerate');
Route::post('/telegram/unlink', [TelegramController::class, 'unlink'])->name('telegram.unlink');
Route::post('/telegram/setup-webhook', [TelegramController::class, 'setupWebhook'])->name('telegram.setup-webhook');
Route::post('/telegram/delete-webhook', [TelegramController::class, 'deleteWebhook'])->name('telegram.delete-webhook');
// Settings
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');

13
supervisor.conf Normal file
View File

@@ -0,0 +1,13 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=/usr/local/sbin/php-fpm
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true