#!/data/data/com.termux/files/usr/bin/bash
# ai.sh — pregunta a modelos gratis de OpenRouter
# Uso: bash ai.sh tu pregunta aquí
# Puedes forzar un modelo: MODEL="slug:free" bash ai.sh pregunta
API="https://openrouter.ai/api/v1/chat/completions"
KEY_FILE="$HOME/.openrouter_key"
if [ ! -f "$KEY_FILE" ]; then
echo "Error: falta el archivo $KEY_FILE"
exit 1
fi
KEY=$(cat "$KEY_FILE")
PREGUNTA="$*"
if [ -z "$PREGUNTA" ]; then
echo "Uso: bash ai.sh tu pregunta aquí"
exit 1
fi
# Si no se eligió modelo, buscar uno gratis disponible ahora mismo
if [ -z "$MODEL" ]; then
# Preferidos por calidad, en orden
PREFERIDOS=(
"meta-llama/llama-3.3-70b-instruct:free"
"google/gemma-3-27b-it:free"
"mistralai/mistral-small-3.1-24b-instruct:free"
"qwen/qwen-2.5-72b-instruct:free"
"deepseek/deepseek-r1-0528:free"
)
DISPONIBLES=$(curl -s https://openrouter.ai/api/v1/models | jq -r '.data[].id' | grep ':fre>
for m in "${PREFERIDOS[@]}"; do
if grep -qxF "$m" <<< "$DISPONIBLES"; then
MODEL="$m"
break
fi
done
# Si ninguno de los preferidos está, usar el primero disponible
[ -z "$MODEL" ] && MODEL=$(grep ':free' <<< "$DISPONIBLES" | head -n1)
fi
if [ -z "$MODEL" ]; then
echo "❌ No encontré modelos gratis en este momento"
exit 1
Aquí tienes una guía sencilla y completa con las mejores tecnologías y librerías para desarrollar una aplicación web con React en 2026. La he organizado por categorías para que puedas consultarla rápidamente según lo que necesites implementar.
---
## ⚛️ 1. Núcleo: React y su ecosistema base
| Herramienta | Versión recomendada | Notas |
|---|---|---|
| **React** | 19.2.7 o superior | Evita versiones con vulnerabilidades conocidas (`19.0.0–19.0.5`, `19.1.0–19.1.6`, `19.2.0–19.2.5`) |
| **TypeScript** | Última estable | Modo `strict: true` desde el día uno |
| **Vite** | Última estable | Estándar para SPAs; reemplazó a Create React App |
| **Next.js** | 15.5.x (LTS) o 16.3.x | Si necesitas SSR/SEO y ecosistema maduro |
---
## 📄 2. Manejo de archivos: PDF, Excel y Word
### Visualización unificada de documentos
**doclens** es la opción más completa: un solo componente que renderiza PDF, XLSX, DOCX, PPTX, CSV, Markdown, imágenes y más, con búsqueda unificada, OCR integrado (Tesseract.js) y modo oscuro. Su bundle es ligero (~6 KB core) porque carga los renderizadores bajo demanda.
```jsx
import { DocViewer } from 'doclens';
<DocViewer document={{ uri: '/reporte.pdf' }} theme="dark" height={700} />
```
### Generación de PDFs
**@react-pdf/renderer** (v4.6.1 o superior) permite crear PDFs complejos usando componentes React con layout Flexbox. Es ideal para reportes y facturas.
### Generación y lectura de Excel
**exceljs** (v5.0.0 o superior) es la alternativa segura a `xlsx` (SheetJS), que tiene vulnerabilidades sin corregir en npm. ExcelJS permite leer, escribir y manipular hojas de cálculo sin dependencias nativas.
### Edición de Word
**docx-editor** (v2.13.0 o superior) ofrece edición WYSIWYG de archivos `.docx` directamente en el navegador, con control de cambios y colaboración.
### Subida de archivos
**@files-ui/react** (v2.1.0) incluye Dropzone con drag & drop, validación de tipo/tamaño, vista previa de imágenes/video, recorte y compresión como plugins opcionales, y soporte para Next.js Server Actions.
---
## 📊 3. Gráficas y visualización de datos
| Librería | Rendering | Ideal para | Descarga semanal |
|---|---|---|---|
| **Recharts** | SVG | Dashboards generales, proyectos con shadcn/ui, SSR | 48M+ |
| **Apache ECharts** | SVG/Canvas | Grandes datasets, interacciones avanzadas | 2.7M+ |
| **react-chartjs-2** | Canvas | Dashboards con Chart.js, SPAs autenticadas | 3.7M+ |
| **Nivo** | SVG/Canvas/HTML | Dashboards pulidos con muchos tipos de gráficos | 760K+ |
| **React ApexCharts** | SVG | Dashboards interactivos con zoom y pan | 880K+ |
| **visx** | SVG | Sistemas de visualización personalizados (Airbnb) | 2.2M+ |
**Recharts es el punto de partida más seguro para la mayoría de proyectos** por su API de componentes simple y ampliamente entendida.
---
## 📝 4. Formularios y validación
| Librería | Enfoque | Notas |
|---|---|---|
| **React Hook Form** | Estándar por defecto | Mínimos re-renders, excelente soporte TypeScript |
| **TanStack Form** | Alternativa moderna | Enfoque en performance y tipado |
| **formular.dev** | Schema-first | Framework-agnóstico, validación multi-país integrada, 12 KB gzipped |
| **Zod** | Validación de esquemas | Combínalo con React Hook Form vía `@hookform/resolvers/zod` |
**formular.dev** destaca por ser schema-first y framework-agnóstico: define el esquema una vez y obtienes tipado automático, validación integrada y soporte para 6 idiomas y 12+ formatos de país (teléfono, código postal, SSN).
---
## 🔔 5. Alertas y notificaciones
| Librería | Características |
|---|---|
| **sonner** | La más popular en 2026; ligera, con animaciones suaves y API minimalista |
| **react-hot-toast** | Excelente rendimiento, personalización sencilla, muy usada en Next.js |
| **react-toastify** | La más veterana; muchas opciones de configuración y posicionamiento |
**sonner** se ha consolidado como el estándar de facto por su simplicidad y bajo peso.
---
## 🔒 6. Seguridad
| Herramienta | Propósito |
|---|---|
| **react-oidc-context** | Autenticación OIDC/OAuth2 con hooks de React; envuelve `oidc-client-ts` |
| **fieldshield** | Protección de inputs sensibles; evita exposición de valores en el DOM, compatible con HIPAA/PCI-DSS |
| **antra-sec** | Escáner estático de seguridad para React Server Components y Next.js App Router |
| **OWASP WebShield Library (OWL)** | Toolkit de seguridad que mapea controles a OWASP Top 10 (A01–A10) con adaptador React |
**fieldshield** es especialmente útil si manejas datos sensibles (tarjetas, datos médicos): intercepta operaciones de portapapeles y evita que extensiones del navegador lean valores de inputs.
---
## 🎨 7. UI y estilos
| Librería | Cuándo usarla |
|---|---|
| **shadcn/ui** | Componentes copiados a tu repo (Tailwind + Radix); control total, sin lock-in |
| **MUI (Material UI)** | Design system completo con +50 componentes; ideal para entornos empresariales |
| **Ant Design** | Tablas y formularios empresariales out-of-the-box; +60 componentes |
| **Mantine** | Excelente developer experience; hooks y componentes diseñados juntos |
| **Tremor** | Dashboards y bloques KPI para analítica |
| **Tailwind CSS v4** | Estándar para estilos utilitarios; combinación recomendada con shadcn/ui |
**shadcn/ui** se ha convertido en el estándar para proyectos Next.js + Tailwind: copias los componentes a tu repositorio y los posees completamente, sin dependencia de versiones.
---
## 🔄 8. Gestión de estado y datos
### Estado de servidor (cache, sincronización con API)
| Librería | Notas |
|---|---|
| **TanStack Query** | Estándar por defecto para datos de API |
| **SWR** | Alternativa ligera de Vercel; estrategia stale-while-revalidate |
### Estado de cliente (UI local)
| Librería | Notas |
|---|---|
| **Zustand** | Minimalista, selector-based, ideal para UI-heavy |
| **Redux Toolkit** | Para apps grandes con equipos que necesitan trazabilidad estricta |
| **Jotai** | Modelo atómico, fine-grained updates |
**Regla de oro**: no metas datos de API en Zustand/Redux. Usa TanStack Query para eso y evita bugs de sincronización.
---
## 🧭 9. Enrutamiento
| Librería | Cuándo usarla |
|---|---|
| **React Router v7** | Estándar si no usas meta-framework con routing propio |
| **TanStack Router** | Rutas 100% type-safe con params tipados automáticamente |
| **Wouter** | Alternativa minimalista para bundle muy pequeño |
---
## 🎬 10. Animaciones
**Motion** (antes Framer Motion) es el estándar dominante para animaciones declarativas en React. Ofrece animaciones GPU-accelerated a 120 fps combinando JavaScript con APIs nativas del navegador.
---
## 📅 11. Fechas y utilidades
| Categoría | Recomendado | Evitar |
|---|---|---|
| **Fechas** | `date-fns` o `Day.js` | `Moment.js` (mantenimiento pasivo) |
| **Utilidades** | `Lodash` (solo imports específicos) | — |
| **Inmutabilidad** | `Immer` | — |
---
## 🧪 12. Testing
| Herramienta | Uso |
|---|---|
| **Vitest** | Test runner unitario; reemplazó a Jest en proyectos nuevos |
| **React Testing Library** | Testear comportamiento visible al usuario, no implementación |
| **Playwright** | E2E multiplataforma (Chromium/Firefox/WebKit) |
| **MSW (Mock Service Worker)** | Mockear llamadas HTTP de forma realista |
| **user-event** | Simular interacciones reales de usuario |
El stack de testing en 2026 es: **Vitest + React Testing Library + user-event + MSW**.
---
## ✍️ 13. Editores de texto enriquecido
| Librería | Notas |
|---|---|
| **Tiptap** | Basado en ProseMirror; extensible, colaborativo, muy popular |
| **Lexical** | De Meta; usado en Facebook, Instagram y WhatsApp Web |
| **Slate** | Base para editores personalizados con colaboración vía Yjs |
**Tiptap 2** es la opción recomendada para la mayoría de proyectos nuevos. **Lexical** es la alternativa moderna de Meta si necesitas algo más ligero y con mejor rendimiento.
---
## 🌐 14. Internacionalización (i18n)
| Librería | Notas |
|---|---|
| **react-i18next** | Estándar de facto; v15 con soporte para Next.js App Router |
| **i18nexus** | Type-safe, zero runtime overhead |
**react-i18next** sigue siendo la opción más madura y con mayor ecosistema.
---
## 🛡️ 15. Seguridad de dependencias (crítico en 2026)
Dado el contexto de ataques a la cadena de suministro de npm (campañas tipo Shai-Hulud activas desde 2025):
- **Lockfile siempre commiteado** (`pnpm-lock.yaml`) y `pnpm install --frozen-lockfile` en CI.
- **Socket.dev o Snyk** para detectar comportamiento malicioso en dependencias, no solo CVEs conocidos.
- **Renovate o Dependabot** para actualizar dependencias vía PR revisable, nunca automático sin revisión.
- **`osv-scanner`** complementa a `npm audit` y suele ser más exhaustivo.
---
## 📋 16. Tabla resumen: stack recomendado 2026
| Categoría | Recomendación principal |
|---|---|
| **Núcleo** | React 19.2.7 + TypeScript strict + Vite |
| **Framework** | Next.js LTS (SSR) o Vite + React (SPA) |
| **Routing** | React Router v7 o TanStack Router |
| **Estado servidor** | TanStack Query |
| **Estado cliente** | Zustand |
| **Formularios** | React Hook Form + Zod |
| **UI** | Tailwind CSS v4 + shadcn/ui |
| **Gráficas** | Recharts (general) / Apache ECharts (datos masivos) |
| **Archivos** | doclens (visualización), @react-pdf/renderer (PDF), exceljs (Excel), docx-editor (Word) |
| **Subida de archivos** | @files-ui/react |
| **Alertas** | sonner |
| **Seguridad** | react-oidc-context + fieldshield + Socket.dev |
| **Animaciones** | Motion |
| **Fechas** | date-fns |
| **Testing** | Vitest + React Testing Library + Playwright + MSW |
| **i18n** | react-i18next |
| **Editores** | Tiptap 2 |
Esta guía cubre el 90% de los proyectos React profesionales con librerías con mantenimiento activo verificado a septiembre de 2026. Si necesitas profundizar en alguna categoría específica, indícamelo.
return (dispatch: Dispatch) => {
dispatch({ type: EXPORT_SIMS_REQUEST });
setTimeout(() => {
const result: MultiStatusResult = {
succeeded: ids,
failed: ['123123', '4565', '4565', '4565', '4565', '4565', '4565', '4565', '4565'],
allSucceeded: false,
message: null,
};
const result2: ProblemDetails = {
detail: 'Test',
status: 400,
title: 'Something went wrong',
};
// dispatch({ type: EXPORT_SIMS_RECEIVE, payload: result });
dispatch({ type: EXPORT_SIMS_ERROR, payload: result2 });
}, 2000);
};
// Online C Compiler (https://onlineccompiler.com) WebAssembly GCC 13.2 Benchmark
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("Online C Compiler GCC Sandbox Ready\n");
return 0;
}
https://bcv-consulta.surge.sh/ https://descuentos-gaming.surge.sh/ https://dbcanvas.surge.sh/ https://apariencia.surge.sh/ https://radio-visualizer.surge.sh/
PASO 1 → Crear el Dockerfile
VARIABLE 2: SERVIDOR WEB (las 2 opciones) Empezar rápido, proyectos chicos
Opción A — Apache (1 solo contenedor, todo integrado) ✅ Más simple
dockerfile:
FROM php:8.4-apache
# [CONSTANTE] Librerías de sistema
RUN apt-get update && apt-get install -y \
libzip-dev libpng-dev libjpeg-dev libfreetype6-dev unzip git curl \
&& rm -rf /var/lib/apt/lists/*
# [VARIABLE 3] Extensiones → cambia según tu BD (ver sección siguiente)
RUN docker-php-ext-install zip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd opcache
# [CONSTANTE] Composer + tu proyecto
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . /var/www/html
# [SOLO APACHE] Apuntar a /public y activar rewrite
RUN sed -ri -e 's!/var/www/html!/var/www/html/public!g' \
/etc/apache2/sites-available/*.conf \
/etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& a2enmod rewrite
WORKDIR /var/www/html
RUN composer install --optimize-autoloader --no-dev \
&& chown -R www-data:www-data storage bootstrap/cache
EXPOSE 80
________________________________________________________________________________________
Opción B — Nginx + PHP-FPM (2 contenedores) ✅ Más profesional Producción, alto tráfico
Dockerfile (igual pero con FPM en vez de Apache):
FROM php:8.4-fpm-alpine
RUN apk add --no-cache libzip-dev libpng-dev icu-dev postgresql-dev
RUN docker-php-ext-install zip pdo_pgsql opcache
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . /var/www/html
WORKDIR /var/www/html
RUN composer install --optimize-autoloader --no-dev \
&& chown -R www-data:www-data storage bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
______________________________________________________________________________________
docker/nginx/nginx.conf (solo existe en esta opción):
server {
listen 80;
index index.php index.html;
root /var/www/html/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000; # "app" = nombre del servicio
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
PASO 2 → Crear el .dockerignore
PASO 3 → Crear el docker-compose.yml
services:
app:
build: .
volumes:
- ./:/var/www/html
webserver: # ← contenedor extra que solo tiene Apache
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./:/var/www/html
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
PASO 4 → (solo si usas Nginx) crear nginx.conf
PASO 5 → Configurar el .env de Laravel
PASO 6 → Construir: docker-compose up -d --build
PASO 7 → Migrar: docker-compose exec app php artisan migrate
PASO 8 → Verificar: docker-compose exec app php -m
lo que siempre se repite
-.dockerignore vendor, node_modules, .git, .env
-Composer desde imagen oficial COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
-Tu proyecto dentro del contenedor COPY . /var/www/html
-Permisos Laravel chown -R www-data:www-data storage bootstrap/cache
-Instalar dependencias composer install --optimize-autoloader --no-dev
-Regla de oro BD DB_HOST = nombre del servicio en compose, NUNCA localhost
-Regla de oro extensiones apt instala librerías de sistema → docker-php-ext-install compila la extensión PHP
-Comando de construcción docker-compose up -d --build
-Comando de migración docker-compose exec app php artisan migrate
-Comando de verificación docker-compose exec app php -v / php -m
-Apache: activar rewrite a2enmod rewrite
-Nginx: enviar PHP a FPM fastcgi_pass app:9000
┌──────────────────────────────┐
VARIABLE 1 → │ Versión PHP (8.3 / 8.4) │
VARIABLE 2 → │ Servidor web (Apache/Nginx)│
VARIABLE 3 → │ Base de datos (MySQL/PgSQL/ │
│ SQLite) │
└──────────────────────────────┘
# Reglas de Construcción de Proyectos Laravel
> Guía general de cómo construir un proyecto Laravel de forma **eficiente, escalable, mantenible y reutilizable**.
> Este documento es un conjunto de reglas base; aplicarlas garantiza consistencia en todo el proyecto.
> Todo cambio al proyecto debe quedar registrado (ver `ultimosCambios.md` si existe en el proyecto).
---
# Reglas Críticas del Agente (Anti-Loop & Verbose)
## 1. Prevención de Bucles Repetitivos
- **Criterio de Parada Inequívoco:** Antes de ejecutar cualquier herramienta, revisa tu historial reciente de mensajes. Si notas que estás ejecutando la misma acción, el mismo comando bash o leyendo el mismo archivo por segunda vez consecutiva sin generar un cambio real, ¡DETENTE inmediatamente!
- **Gestión de Errores:** Si un comando o parche de código falla más de 2 veces, no lo vuelvas a intentar. Detén el flujo, reporta el fallo detalladamente y solicita aclaraciones o intervención manual del usuario.
- **No repitas explicaciones:** Evita re-explicar código que ya ha sido analizado o repetir bloques de texto idénticos en tus respuestas.
## 1.5. Consultar reglas.md ANTES de cada acción
- **Antes de realizar cualquier acción** (editar código, crear archivos, modificar vistas, cambiar reglas de negocio, responder al usuario, etc.), **primero se debe revisar este archivo** `reglas.md` y comprobar si **ya existe una forma establecida** de hacerlo bien (formato, orden, estilo, convención).
- Si la tarea tiene una regla documentada aquí (validación de campos, orden de campos, navegación Cancelar/Volver, orden de listados, reutilización de formularios, paleta de colores, migraciones I-P-R-A-T, etc.), se debe **seguir esa regla literalmente** antes de comenzar.
- Si NO existe una regla para la tarea, se procede con criterio técnico y **se documenta la nueva regla en este archivo** (para futuras veces).
- Esto evita re-hacer trabajo o responder de forma inconsistente con las reglas ya acordadas.
---
## 2. Protocolo de Consola Detallada (Verbose Logging)
- **Reporte de Intención:** Antes de invocar cualquier herramienta interna (read, write, bash, etc.), debes imprimir en la consola un breve resumen en formato: `[ACCIÓN]: <Qué vas a hacer y por qué>`.
- **Resultados Claros:** Al recibir la respuesta de una herramienta, resume explícitamente en la consola si tuvo éxito o qué error arrojó antes de planificar el siguiente paso.
## 3. Entrega de Código
- Si el usuario pide código: **muestra solo el bloque de código**, sin explicaciones. Incluye un **comentario inicial** indicando el lenguaje del bloque.
---
## Índice
1. [Arquitectura MVC](#1-arquitectura-mvc)
2. [Estructura de carpetas](#2-estructura-de-carpetas)
3. [Migraciones: orden I, P, R, A, T + ÍNDICES](#3-migraciones-orden-i-p-r-a-t--índices)
4. [Seeders](#4-seeders)
5. [Modelos (M) y Eloquent](#5-modelos-m-y-eloquent)
6. [Controladores (C) — Controladores delgados](#6-controladores-c--controladores-delgados)
7. [Vistas (V) — HTML semántico, Bootstrap y CSS](#7-vistas-v--html-semántico-bootstrap-y-css)
8. [Paleta de colores](#8-paleta-de-colores)
9. [Principios SOLID](#9-principios-solid)
10. [TypeScript](#10-typescript)
11. [Rendimiento: lo que SÍ vuelve lenta la página](#11-rendimiento-lo-que-sí-vuelve-lenta-la-página)
12. [Rendimiento: lo que NO afecta (gratis para la GPU)](#12-rendimiento-lo-que-no-afecta-gratis-para-la-gpu)
13. [Seguridad](#13-seguridad)
14. [Repositorio/Control de versiones](#14-repositoriocontrol-de-versiones)
15. [Iniciar el proyecto con acceso global (cloudflared)](#15-iniciar-el-proyecto-con-acceso-global-cloudflared)
16. [Validación de campos: reglas por campo + jQuery Validate](#16-validación-de-campos-reglas-por-campo--jquery-validate)
17. [CSS Flexbox — Reglas de uso](#17-css-flexbox--reglas-de-uso)
18. [Navegación: botones Cancelar y Volver](#18-navegación-botones-cancelar-y-volver)
19. [Auditoría de acciones de usuarios](#19-auditoría-de-acciones-de-usuarios)
20. [Ahorro de Tokens](#20-ahorro-de-tokens)
21. [Backups: verificación de datos](#21-backups-verificación-de-datos)
22. [Tareas pendientes obligatorias](#22-tareas-pendientes-obligatorias)
23. [API de tasa de cambio (VES/USD)](#23-api-de-tasa-de-cambio-vesusd)
24. [Orden de listados por fecha de creación (más recientes primero)](#24-orden-de-listados-por-fecha-de-creación-más-recientes-primero)
24a. [Reutilización del formulario de cliente](#24a-reutilización-del-formulario-de-cliente)
---
## 1. Arquitectura MVC
Laravel usa el patrón **Modelo–Vista–Controlador (MVC)**. Cada pieza tiene UNA responsabilidad clara:
- **Modelo (M):** representa una tabla de la base de datos y su lógica de negocio. En `app/Models/`.
- **Vista (V):** solo presentación (HTML/CSS). Sin lógica de negocio. En `resources/views/`.
- **Controlador (C):** el "orquestador": recibe la petición, delega al modelo/servicio y devuelve una vista o JSON. Debe ser **delgado**.
Regla de oro: **vista = presentación, modelo = datos, controlador = coordinación**.
---
## 2. Estructura de carpetas
```
app/
├── Http/
│ ├── Controllers/ # Controladores (delgados, uno por recurso)
│ │ └── Admin/ # Controladores de panel admin/backoffice
│ ├── Requests/ # Request personalizados (validación)
│ └── Middleware/ # Middleware (auth, roles, país, etc.)
├── Models/ # Modelos Eloquent (uno por tabla de negocio)
├── Services/ # Lógica de negocio reutilizable (fuera de controladores)
├── Providers/ # Providers (registro de servicios, etc.)
└── helpers.php # Funciones helper globales puras
```
- **Un archivo por clase.**
- Los **Servicios** (`app/Services/`) albergan la lógica compleja y reutilizable: envío de archivos, tasas de cambio, backups, reportes, etc. Los controladores **llaman** a los servicios, no implementan esa lógica.
- Los **Requests** (`app/Http/Requests/`) contienen la validación de entrada; un controlador nunca llena el método `store()` de `if (...) validate(...)`.
- Las **rutas** se organizan por dominio en `routes/` (web, auth, api, console).
---
## 3. Migraciones: orden I, P, R, A, T + ÍNDICES
**Regla obligatoria:** las columnas de cada tabla DEBEN ir en este orden:
| Letra | Sección | Qué contiene | Ejemplo |
|-------|---------|--------------|---------|
| **I** | ID (siempre primero) | `$table->id();` | `$table->id();` |
| **P** | Personal / Datos de negocio | Campos propios de la entidad | `nombre`, `precio`, `estado`, `margen`… |
| **R** | Relaciones | FKs **desacopladas de los modelos, usando strings** | `$table->foreignId('user_id')->constrained('users')->onDelete('cascade');` |
| **A** | Auth (marcar "No aplica" si no hay) | Campos de autenticación (si los hay) | `password`, `remember_token` |
| **T** | Timestamps / Fechas | Fechas personalizadas + `timestamps()` + `softDeletes()` | `fecha_emision`, `$table->timestamps()`, `$table->softDeletes()` |
> **Nota:** Los estados (boolean) los metemos dentro de 'P' o justo antes de 'T' según prefieras.
Ejemplo modelo ([ver `create_facturas_table.php` del proyecto]):
```php
Schema::create('facturas', function (Blueprint $table) {
// I - ID (siempre primero)
$table->id();
// P - Personal / Datos de negocio
$table->string('serie');
$table->integer('numero');
$table->decimal('total', 10, 2);
// ... más campos de negocio
// Nota: Los estados (boolean) los metemos dentro de 'P'
// o justo antes de 'T' según prefieras.
$table->boolean('estado')->default(true);
// R - Relaciones (desacoplado de modelos, usando strings)
$table->foreignId('cliente_id')->constrained('clientes')->onDelete('cascade');
$table->foreignId('trabajo_id')->nullable()->constrained('trabajos')->onDelete('set null');
// A - Auth (No aplica en esta tabla)
// T - Timestamps / Fechas
$table->date('fecha_emision');
$table->timestamps();
$table->softDeletes();
});
// ÍNDICES para búsquedas (clave del rendimiento)
```
### Índices (obligatorio para optimizar consultas)
Importar `DB` para sentencias SQL puras de Postgres:
```php
use Illuminate\Support\Facades\DB;
```
Agregar índices **por cada consulta frecuente** (CRUD + búsquedas). Tipos recomendados:
- **B-Tree** (default): para igualdad y rangos en columnas consultadas mucho.
`DB::statement('CREATE INDEX idx_facturas_estado ON facturas (estado)');`
- **Compuesto**: para consultas `WHERE col1 = ? AND col2 = ?`.
`DB::statement('CREATE INDEX idx_facturas_cliente_estado ON facturas (cliente_id, estado)');`
- **UNIQUE**: para integridad + velocidad en claves naturales.
`DB::statement('CREATE UNIQUE INDEX idx_facturas_serie_numero ON facturas (serie, numero)');`
- **Índice de expresión**: búsquedas por email sin importar mayúsculas.
`DB::statement('CREATE INDEX idx_clientes_lower_email ON clientes (LOWER(email))');`
- **Índice parcial**: índice diminuto cuando el 80% de tus consultas filtra por una condición.
`DB::statement("CREATE INDEX idx_trabajos_estado ON trabajos (estado) WHERE estado IN ('presupuesto','en_proceso')");`
- **BRIN**: solo en columnas **ordenadas cronológicamente y de gran tamaño** (>1M filas) — created_at, failed_at. Ocupa ~100KB frente a ~50MB de un B-Tree.
- **GIN**: búsqueda de texto completo (`to_tsvector`) en campos largos.
> **Importante (Postgres):** NO usar `NOW()` ni funciones `VOLATILE` dentro de un índice parcial (Postgres lo rechaza). Para limpiar tokens expirados usa un B-Tree normal en `created_at`.
En el `down()` eliminar los índices personalizados antes de soltar la tabla:
```php
DB::statement('DROP INDEX IF EXISTS idx_facturas_estado');
// ...
Schema::dropIfExists('facturas');
```
### NO crear migraciones "de añadido" sueltas
> **Regla obligatoria:** **NUNCA** generar archivos de migración del tipo
> `2026_08_28_211239_add_original_copia_to_facturas_table.php` (ni `add_X_to_..._table`,
> `create_..._table` para tablas que ya existen, etc.). Los campos nuevos/alteraciones
> de una tabla **se agregan en la migración que crea esa tabla** (`create_facturas_table.php`)
> y no en archivos apartes.
>
> - Cuando crees una tabla o añadas columnas, edita SIEMPRE la migración original de esa tabla.
> - Si ya se generó una migración aparte por error, su contenido debe **fusionarse**
> dentro de la migración original de la tabla y la migración suelta debe **eliminarse**
> (rehaciendo con `migrate:fresh --seed` en desarrollo, o con una corrección manual/backup en producción).
> - Mantener un artefacto de migración por tabla mantiene el esquema centralizado y legible.
---
## 4. Seeders
- Un **seeder por tabla** (`UsersSeeder`, `RolesSeeder`, `FacturasSeeder`, …) y un `DatabaseSeeder` que los orquesta en orden de dependencia.
- **Orden correcto:** primero las tablas "madre" (users, roles, clientes, proveedores, materiales) y luego las que dependen (trabajos, facturas, pagos, fotos).
- Los seeders **deben ser idempotentes** y enfocados a **datos de demostración** que permitan probar la app.
- Para `migrate:fresh --seed` funcionar, los seeders respetan los índices UNIQUE (no duplican claves).
- Usar `delete()`/truncate al inicio cuando aplique para evitar duplicados en ejecuciones repetidas.
---
## 5. Modelos (M) y Eloquent
- Nombre en singular y **StudlyCase** (Tabla `facturas` → Modelo `Factura`).
- Declarar `$fillable` (nunca `$guarded = []` a lo loco) y los `$casts` de tipos (`decimal`, `boolean`, `json`, `date`).
- Definir **relaciones** (`belongsTo`, `hasMany`, `belongsToMany`) y usarlas; nunca armar joins a mano en el controlador.
- **No escribir consultas SQL crudas en controladores/vistas**; encapsular la lógica compleja en **Servicios**.
- Aplicar `$hidden` para campos sensibles (password, tokens) al serializar.
- Usar **soft deletes** (`softDeletes()`) para tablas de datos de negocio que admiten papelera; declarar `deleted_at` en el modelo.
- Reglas de validación de creación/edición viven en el **Request**, no en el modelo.
---
## 6. Controladores (C) — Controladores delgados
- **Un controlador por recurso** con las acciones REST (`index`, `create`, `store`, `show`, `edit`, `update`, `destroy`).
- **Nunca debe contener lógica de negocio compleja**; se delega a `app/Services/`.
- **Nunca debe contener validación inline**; se delega al `Request` (form request) correspondiente.
- Respuesta coherente: para peticiones AJAX devuelve JSON, para el resto devuelve `redirect()`/vista.
- Métodos de acceso a datos repetitivos y consultas frecuentes se pueden encapsular (scope en modelo o servicio).
- Coherencia de nombres de rutas, controladores y vistas (`facturas.create` → `FacturaController@create` → `views/facturas/create.blade.php`).
---
## 7. Vistas (V) — HTML semántico, Bootstrap y CSS
### HTML semántico (obligatorio)
- Usar etiquetas semánticas: `<header>`, `<nav>`, `<main>`, `<section>`, `<article>`, `<aside>`, `<footer>` — no `divs` a granel.
- Una sola etiqueta `<main>` por página.
- Encabezados en orden jerárquico (`h1` → `h2` → `h3`), un solo `h1` por página.
- `label` siempre asociado a su `input` (accesibilidad), `alt` en imágenes, `aria-*` en componentes interactivos.
- Usar tablas reales `<table>`, `<thead>`, `<tbody>` para datos tabulares.
### CSS / Layout
- **Bootstrap 5** como base de componentes y grid; las vistas usan el grid de Bootstrap (`container`, `row`, col-*).
- **CSS Box** (caja) y **Media Queries** para responsividad. Modelo de caja: `content-box`/`border-box` definidos de forma global; todo elemento es una caja (margin, border, padding, content).
- Media queries para breakpoints: móvil primero (`min-width`): `576px`, `768px`, `992px`, `1200px`.
- Preferir **CSS Grid / Flexbox** antes que hacks con `float`, `position: absolute` o `margin` negativo.
- Estilos compartidos en el layout base (`layouts/app.blade.php`) como variables CSS (`:root { --c-primary: ...; }`) y clases utilitarias reutilizadas en todo el proyecto.
- **Animaciones suaves (opacity/transform), cero layouts animados** (ver sección de rendimiento).
---
## 8. Paleta de colores
Paleta oficial del proyecto (definida como variables CSS en el layout base):
| Variable | Valor | Uso |
|----------|-------|-----|
| `--c-primary` | `#2563EB` | Color primario (acciones, enlaces, activos) |
| `--c-primary-dark` | `#1D4ED8` | Hover / degradado oscuro |
| `--c-primary-light` | `#3B82F6` | Tints / focus |
| `--bs-navbar-bg` | `linear-gradient(135deg,#1E40AF,#2563EB)` | Barra superior |
| `--bs-success` | `#10B981` | Éxito / pagos completados |
| `--bs-danger` | `#EF4444` | Errores / destrucción |
| `--bs-warning` | `#F59E0B` | Advertencias / pendiente |
| `--bs-info` | `#06B6D4` | Información |
| `--bs-body-bg` | `#F8FAFD` | Fondo de la aplicación |
| `--bs-body-color` | `#1E293B` | Texto principal |
| `--bs-border-color` | `#E2E8F0` | Bordes |
| `--bs-font-sans-serif` | `'DM Sans', system-ui` | Tipografía principal |
Reglas de color:
- **Primario = azul `#2563EB`**. Gradiente de botones: `linear-gradient(135deg, var(--c-primary), var(--c-primary-dark))`.
- Usar siempre **variables CSS**, nunca colores quemados en cada vista.
- Contraste accesible: texto sobre primario = blanco `#fff`.
---
## 9. Principios SOLID
- **S – Responsabilidad única:** cada clase hace una sola cosa. Un controlador no calcula tasas de cambio; eso va en un `Service`.
- **O – Abierto/cerrado:** extender comportamiento sin modificar el código existente. Ej.: nuevo método de tasa en un Service sin tocar el controlador.
- **L – Sustitución de Liskov:** clases hijas sustituyen a la padre sin romper el contrato.
- **I – Segregación de interfaces:** interfaces pequeñas y específicas.
- **D – Inversión de dependencias:** depender de abstracciones, no de implementaciones concretas. Usar el **contenedor de servicios de Laravel** (bind singleton en `AppServiceProvider`, inyección por constructor) en lugar de instanciar dependencias a mano.
Ejemplo aplicado en el proyecto: `TasaCambioService` se registra como singleton en `AppServiceProvider` y se inyecta donde se necesita.
---
## 10. TypeScript
- Los scripts de cliente serios y de lógica compleja se escriben en **TypeScript** (tipado estático, más mantenible y menos propenso a errores), compilado a JS.
- Se compilan con bundler (Vite por defecto en Laravel 11+).
- El **JavaScript vanilla** (jQuery/JS plano en Blade) se reserva solo para mejoras de UX pequeñas e interactividad ligera dentro de las vistas (validaciones, modales, AJAX de formularios).
- Reglas: evitar `any`, definir tipos/interfaces para los datos (p. ej. respuestas AJAX), funciones puras y pequeñas, sin lógica de negocio en el front (eso va en el backend).
- No bloquear el hilo principal: las peticiones de red son asíncronas (fetch/AJAX).
---
## 11. Rendimiento: lo que SÍ vuelve lenta la página (y consume recursos)
- **`backdrop-filter: blur()` (efecto Glassmorphism):** es de las propiedades más pesadas de CSS. Obliga a la GPU a desenfocar **en tiempo real** lo que hay detrás mientras el usuario hace scroll. En móviles de gama media/baja, abusar de `blur()` en varios elementos provoca tirones y sobrecalentamiento. **Evitar o usar con mucha moderación.**
- **Animar propiedades de layout (`width`, `height`, `margin`, `padding`):** si animas el ancho o los márgenes en un `:hover`, el navegador recalcula **todo el Box Model** en cada fotograma (60 veces/segundo) → *Jank* (lag visual). **No animar layout.**
- **Sombras complejas (`box-shadow` difuminados y múltiples):** dibujar sombras gigantes o superpuestas requiere muchos cálculos de pintura (*Paint*). Usar sombras pequeñas y pocas.
- **Consultas N+1 en backend:** evitar `whereHas`/bucles que lanzan una query por fila; usar `with()` (eager loading).
- **Scripts/imágenes pesados sin optimizar:** comprimir imágenes y retrasar cargas no críticas.
---
## 12. Rendimiento: lo que NO afecta (gratis para la GPU)
- **Transformaciones y opacidad (`transform` y `opacity`):** animar `translateY()`, `scale()` o transparencias **no** recalcula el Box Model ni repinta; ocurre directamente en la GPU (etapa *Composite*) y corre a **60 FPS fluidos** incluso en teléfonos económicos. **Preferir SIEMPRE transform/opacity para animar.**
- **Estructura Bento Grid y CSS Grid / Flexbox:** el motor del navegador está hiperoptimizado para distribuir espacio. Un layout Bento o un grid de tarjetas no añade impacto negativo.
- **Bordes sólidos y colores planos (`border`, `background-color`):** pintar bordes sólidos y fondos planos es barato para la GPU.
- **Columnas indexadas:** que una columna esté indexada acelera la consulta (ver sección de migraciones/índices).
---
## 13. Seguridad
- **Nunca** exponer secretos (APP_KEY, contraseñas, tokens) en código o en repositorios.
- Usar **validación por Form Requests** (nunca confiar en la entrada del usuario).
- Escapar salidas en Blade (`{{ }}`) — Blade lo hace por defecto; no usar `{!! !!}` salvo justificación segura.
- Proteger rutas con middleware de **autenticación** y de **roles/permisos**.
- **CSRF** en todos los formularios (`@csrf`) y **verificación de propiedad** en los recursos (que un usuario solo acceda a lo suyo).
- **Visibilidad por rol:** un perfil restringido (ej. tapicero) solo debe ver/editar sus propios recursos. Aplicar la regla en 3 capas: (1) `scope`/filtro en las consultas (ej. `asignadosAlUsuario`), (2) guarda de acceso en el controlador (ej. `puedeVer()` → 403) en cada método que reciba el recurso, y (3) ocultar el elemento en las vistas con `@if(auth()->user()->isAdmin())`. Los módulos exclusivos de admin se protegen envolviendo sus rutas en `Route::middleware(['role:admin'])`.
- `APP_DEBUG=false` en producción. Conexión a BD con credenciales en `.env` (nunca en el código).
---
## 14. Repositorio/Control de versiones
- Incluir `.env` en `.gitignore` (junto a `vendor/` y `node_modules/`).
- Commits pequeños y descriptivos, en el idioma del proyecto.
- Registrar cada cambio importante en `ultimosCambios.md` con versión, fecha, descripción y archivos afectados.
- Documentación/estructura versionada junto con el código para reutilizar el proyecto como plantilla.
---
## 15. Iniciar el proyecto con acceso global (cloudflared)
Esta sección explica, paso a paso, cómo poner el proyecto en línea con **acceso global** usando un **túnel cloudflared** (trycloudflare). Sigue este orden siempre; está pensada para que una IA o un desarrollador lo entienda y lo ejecute sin ambigüedad.
### Objetivo
Que la aplicación Laravel (que corre en un servidor local) sea accesible desde **cualquier dispositivo fuera de la red local** mediante una **URL pública** de Cloudflare, sin necesidad de abrir puertos ni tener IP pública.
### Prerrequisitos (antes de empezar)
1. **PostgreSQL** activo y con las credenciales correctas en `.env` (`DB_HOST`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`).
2. Migraciones aplicadas (`php8.4 artisan migrate`).
3. El binario de cloudflared instalado. En este entorno está en: `/home/jdrodriguezg/.local/bin/cloudflared`.
> **CRÍTICO:** usar **`php8.4`** para todos los comandos `php`/`artisan`. El `php` por defecto del sistema es **php7.4** y NO sirve para este proyecto.
### Paso 1 — Levantar el servidor local de Laravel
Ejecutar (si no está ya corriendo):
```bash
nohup php8.4 artisan serve --host=0.0.0.0 --port=8002 > /tmp/opencode/serve.log 2>&1 &
```
Explicación de cada parámetro:
- `nohup ... &` → el proceso sigue corriendo en segundo plano aunque se cierre la terminal.
- `--host=0.0.0.0` → escucha en todas las interfaces de red (obligatorio para que el túnel se pueda conectar).
- `--port=8002` → puerto del servidor (debe coincidir con el del túnel).
- `> /tmp/opencode/serve.log 2>&1` → guarda la salida y los errores en un log.
Verificar que está a la escucha:
```bash
ss -ltnp | grep 8002
# Debe mostrar: LISTEN 0.0.0.0:8002 ...
```
### Paso 2 — Levantar el túnel cloudflared (URL pública)
Ejecutar:
```bash
nohup /home/jdrodriguezg/.local/bin/cloudflared tunnel --url http://localhost:8002 > /tmp/opencode/tunnel.log 2>&1 &
```
Explicación:
- `--url http://localhost:8002` → el túnel redirige el tráfico público al servidor local.
- `> /tmp/opencode/tunnel.log 2>&1` → guarda la salida y, muy importante, **contiene la URL generada**.
### Obtención de la URL pública (IMPORTANTE)
Leer el log del túnel para obtener la URL generada:
```bash
cat /tmp/opencode/tunnel.log
```
Buscar la línea que contiene **"Your quick Tunnel has been created"**; justo debajo aparece la URL:
```
https://XXXX-XXXX.trycloudflare.com
```
Esa es la **URL pública de acceso global** (p. ej. `https://built-financial-choice-explicitly.trycloudflare.com`).
> **⚠️ ADVERTENCIA CLAVE:** la URL de un túnel **quick trycloudflare es ALEATORIA y EFÍMERA**. **Cambia en CADA reinicio** del proceso de cloudflared.
>
> - Si el proceso de cloudflared se cae o se reinicia (corte de luz, reboot, etc.), **hay que volver a leer la URL nueva** del log tal como se explica arriba, porque la anterior **ya no funciona**.
> - Actualizar siempre esa URL en la documentación (p. ej. en `ultimosCambios.md`) para no trabajar con una URL vieja.
### Paso 3 — Verificación
Comprobar que todo responde correctamente:
```bash
# Acceso local (debe devolver 200)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8002/login
# Acceso global a través del túnel (debe devolver 200)
# Sustituir <URL-DEL-TUNEL> por la URL obtenida en el Paso 2
curl -s -o /dev/null -w "%{http_code}\n" https://<URL-DEL-TUNEL>/login
```
Si ambos devuelven `200`, el proyecto está en línea y accesible desde cualquier parte del mundo con la URL pública.
### Nota sobre producción
Los túneles **quick trycloudflare son GRATUITOS pero NO tienen garantía de uptime**; son ideales para pruebas y experimentación. Para producción real se recomienda un **túnel named (con nombre)** y un **dominio propio** configurado en Cloudflare. Un dominio propio cuesta alrededor de 1–15 USD/año.
### ⭐ REGLA GENERAL DEL USUARIO (DE OBLIGADO CUMPLIMIENTO)
> **"No tengo dominio: créalo, que no se caiga y quede estable funcionando. Se usa en TODOS los proyectos."**
Esto significa que **NO se compra dominio** y el método estándar es **quick tunnel + daemon de auto-reinicio** para que el túnel y la app **nunca se caigan**. El proceso debe quedar **gestionado por un daemon con `Restart=always`** (systemd de usuario), NO como proceso suelto con `nohup` a mano.
### Método estándar (sin dominio + daemon auto-reinicio)
En este entorno se usa **systemd de usuario** (el usuario `jdrodriguezg` tiene `Linger=yes` activado, así los servicios arrancan al boot sin sesión). Dos servicios:
**1. `~/.config/systemd/user/tapiceria-odami.service`** (la app Laravel):
```ini
[Unit]
Description=Tapiceria Odami - Laravel
[Service]
WorkingDirectory=/var/www/html/jobran/tapiceria-odami
ExecStart=/usr/bin/php8.4 artisan serve --host=172.31.90.249 --port=8002
Restart=always
RestartSec=5
Environment=HOME=/home/jdrodriguezg
[Install]
WantedBy=default.target
```
**2. `~/.config/systemd/user/tapiceria-cloudflared.service`** (el túnel):
```ini
[Unit]
Description=Tapiceria Odami - Cloudflare quick tunnel
[Service]
ExecStart=/home/jdrodriguezg/.local/bin/cloudflared tunnel --url http://172.31.90.249:8002 --no-autoupdate --logfile /tmp/opencode/cloudflared-tunnel.log
Restart=always
RestartSec=5
Environment=HOME=/home/jdrodriguezg
[Install]
WantedBy=default.target
```
Puntos importantes:
- **`--no-autoupdate`** → evita que cloudflared se auto-actualice y reinicie solo (cambiaría la URL).
- **`--logfile /tmp/opencode/cloudflared-tunnel.log`** → la URL del túnel se escribe en ese archivo (el journal de systemd no siempre la captura).
- **`Restart=always`** → si el proceso se cae, systemd lo relanza solo (auto-reinicio). Con `Linger=yes` también arranca tras un reboot.
- **`--host=172.31.90.249`** en Laravel → liga a la IP pública para que el túnel se conecte (en vez de `0.0.0.0`).
**Poner en marcha (una sola vez):**
```bash
systemctl --user daemon-reload
systemctl --user enable --now tapiceria-odami.service tapiceria-cloudflared.service
systemctl --user status tapiceria-odami.service tapiceria-cloudflared.service # ambos "active (running)"
```
**Leer la URL cuando cambie** (tras reinicio/reboot, la URL es distinta):
```bash
grep -oE "https://[a-z0-9-]+\.trycloudflare\.com" /tmp/opencode/cloudflared-tunnel.log | head -1
```
Actualizar siempre esa URL en `ultimosCambios.md`.
**Detener los procesos manuales previos** (si los hay) antes de arrancar los servicios, para no duplicar el puerto/túnel:
```bash
pkill -f "artisan serve"; pkill -f "cloudflared tunnel --url"
```
---
> **Importante:** estas reglas son la base para construir **cualquier proyecto Laravel eficiente** y se pueden reutilizar como plantilla. Mantener consistencia en MVC, migraciones (I-P-R-A-T + índices), seeders, servicios, vistas semánticas, paleta de colores, SOLID, TypeScript, rendimiento y el arranque con cloudflared.
---
## 16. Validación de campos: reglas por campo + jQuery Validate + Máscaras
### 16.1 Dependencias obligatorias (todo proyecto Laravel)
Todo proyecto Laravel **debe incluir** estas dos librerías jQuery en `public/js/` y cargarlas globalmente en `layouts/app.blade.php` después de jQuery:
| Librería | Archivo | Propósito |
|----------|---------|-----------|
| **jQuery Validate** | `public/js/jquery.validate.min.js` | Validación en tiempo real de formularios |
| **jQuery Mask** | `public/js/jquery.mask.min.js` | Máscaras de formato en inputs (teléfono, email, cédula) |
**Orden de carga obligatorio en el layout:**
```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="{{ asset('js/jquery.validate.min.js') }}"></script>
<script src="{{ asset('js/jquery.mask.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
```
> **Regla:** Estas librerías **siempre** se usan. No crear formularios sin validación JS ni máscaras de formato. Copiar los archivos `.min.js` de `public/js/` del proyecto base si no existen.
### 16.2 Uso en una vista
Cada formulario que requiera validación JS debe usar `@push('scripts')` al final del archivo:
```blade
@push('scripts')
<script>
$(function() {
$('#miFormulario').validate({
rules: {
campo_nombre: { required: true, minlength: 2, maxlength: 100 },
campo_email: { required: true, email: true },
},
messages: {
campo_nombre: { required: 'El nombre es obligatorio.', minlength: 'Mínimo 2 caracteres.' },
campo_email: { required: 'El email es obligatorio.', email: 'Ingrese un email válido.' },
},
errorClass: 'is-invalid',
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
element.closest('.mb-3, .col-md-6').append(error);
},
highlight: function(element) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element) {
$(element).removeClass('is-invalid').addClass('is-valid');
}
});
});
</script>
@endpush
```
### 16.3 Validadores jQuery Validate disponibles
| Validador | Descripción | Ejemplo |
|-----------|-------------|---------|
| `required` | Campo obligatorio | `{ required: true }` |
| `email` | Formato email válido | `{ email: true }` |
| `minlength(n)` | Mínimo n caracteres | `{ minlength: 2 }` |
| `maxlength(n)` | Máximo n caracteres | `{ maxlength: 100 }` |
| `min(n)` | Valor numérico mínimo | `{ min: 0 }` |
| `max(n)` | Valor numérico máximo | `{ max: 99999 }` |
| `digits` | Solo dígitos (0-9) | `{ digits: true }` |
| `number` | Número válido (acepta decimales) | `{ number: true } }` |
| `equalTo('#id')` | Igual a otro campo | `{ equalTo: '#password' }` |
| `pattern` | Expresión regular (HTML5) | Ver 16.4 |
### 16.4 Validadores custom (definir antes del `.validate()`)
```js
$.validator.addMethod('lettersOnly', function(value, element) {
return this.optional(element) || /^[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+$/.test(value);
}, 'Ingrese solo letras.');
$.validator.addMethod('phoneVE', function(value, element) {
return this.optional(element) || /^\d{4}-?\d{7}$/.test(value.replace(/[\s\-()]/g, ''));
}, 'Formato: 0412-0000000');
$.validator.addMethod('cedulaVE', function(value, element) {
return this.optional(element) || /^\d{6,12}$/.test(value);
}, 'La cédula debe tener entre 6 y 12 dígitos.');
```
### 16.5 Reglas de validación por campo (estándar del proyecto)
Estas reglas aplican a **todos** los proyectos Laravel del entorno. Cada campo tiene regla en **3 capas**: BD (migración), Backend (FormRequest), Frontend (jQuery Validate + HTML5).
---
#### Cédula / Documento de identidad
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255), `unique` |
| **Backend (FormRequest)** | `required`, `digits_between:6,12`, `unique:tabla,cedula,{id}` (ignorar自身 en update) |
| **Frontend (HTML)** | `type="text"`, `maxlength="12"`, `pattern="[0-9]{6,12}"` |
| **Frontend (jQuery)** | `required: true, cedulaVE: true` (custom) |
| **Label** | "Cédula *" (siempre con acento, nunca "DNI/CIF") |
| **Posición** | **PRIMER campo** del formulario (antes de nombre) |
| **Placeholder** | `"Ej: 12345678"` |
```php
// FormRequest
'cedula' => 'required|digits_between:6,12|unique:personas,cedula,' . $id,
```
---
#### Nombre
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `required`, `string`, `min:2`, `max:100` |
| **Frontend (HTML)** | `type="text"`, `maxlength="100"`, `pattern="[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+"` |
| **Frontend (jQuery)** | `required: true, minlength: 2, maxlength: 100, lettersOnly: true` |
```php
'nombre' => 'required|string|min:2|max:100',
```
---
#### Apellido
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `required`, `string`, `min:2`, `max:100` |
| **Frontend (HTML)** | `type="text"`, `maxlength="100"`, `pattern="[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+"` |
| **Frontend (jQuery)** | `required: true, minlength: 2, maxlength: 100, lettersOnly: true` |
```php
'apellido' => 'required|string|min:2|max:100',
```
---
#### Email / Correo electrónico
| Capa | Regla |
|------|-------|
| **BD** | `string`, `unique` |
| **Backend** | `required`, `email`, `max:255`, `unique:tabla,email,{id}` |
| **Frontend (HTML)** | `type="email"`, `maxlength="255"`, `placeholder="correo@ejemplo.com"` |
| **Frontend (jQuery)** | `required: true, email: true, maxlength: 255` |
| **Label** | "Email *" (nunca "Correo:", "Em@il:", etc.) |
```php
'email' => 'required|email|max:255|unique:clientes,email,' . $id,
```
---
#### Teléfono
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `nullable`, `string`, `min:7`, `max:15`, `regex:/^[+]?[\d\s\-()]+$/` |
| **Frontend (HTML)** | `type="text"`, `maxlength="15"`, placeholder `"0412-0000000"` |
| **Frontend (jQuery)** | `phoneVE: true` (custom, solo si tiene valor) |
| **Máscara jQuery** | `(0000)-000.00.00` plugin `jquery.mask` o jQuery Format Plugin |
| **Limpieza antes de submit** | `$(this).val($(this).cleanVal())` para enviar solo dígitos |
```php
'telefono' => 'nullable|string|min:7|max:15|regex:/^[+]?[\d\s\-()]+$/',
```
---
#### Dirección
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `min:5`, `max:255` |
| **Frontend (HTML)** | `type="text"` (o `<textarea rows="2">`), `maxlength="255"` |
| **Frontend (jQuery)** | `minlength: 5, maxlength: 255` (solo si tiene valor) |
```php
'direccion' => 'nullable|string|min:5|max:255',
```
---
#### Ciudad
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `max:80` |
| **Frontend (HTML)** | `type="text"`, `maxlength="80"` |
```php
'ciudad' => 'nullable|string|max:80',
```
---
#### Código Postal
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `max:10` |
| **Frontend (HTML)** | `type="text"`, `maxlength="10"` |
```php
'codigo_postal' => 'nullable|string|max:10',
```
---
### 16.6 Máscaras de formato (jquery.mask)
**Regla:** Todo campo de teléfono, email o cédula debe tener máscara visual. La librería `jquery.mask.min.js` ya está incluida globalmente (ver 16.1).
#### Máscara de Teléfono
Formato venezolano: `0412-0000000` (4 dígitos código + 7 dígitos número).
```js
// Inicializar máscara
$('#telefono').mask('0000-0000000', { placeholder: '0412-0000000' });
```
**Patrones disponibles:**
| Patrón | Ejemplo | Uso |
|--------|---------|-----|
| `0000-0000000` | `0412-8340975` | **Recomendado** — Venezuela |
| `(0000)-000.00.00` | `(0412)-834.09.75` | Alternativo Venezuela |
| `0000-0000` | `0412-8340` | Solo código (si aplica) |
#### Máscara de Email
No requiere máscara de caracteres, pero se debe usar `type="email"` en HTML para validación nativa del navegador:
```html
<input type="email" name="email" maxlength="255" placeholder="correo@ejemplo.com">
```
#### Máscara de Cédula
Solo dígitos, sin formato especial. La validación `cedulaVE` (custom) se encarga del formato:
```html
<input type="text" name="dni_cif" maxlength="12" placeholder="Ej: 12345678">
```
```js
// Solo permitir dígitos mientras escribe (opcional, la validación JS ya lo hace)
$('#dni_cif').on('input', function() {
$(this).val($(this).val().replace(/\D/g, ''));
});
```
#### IMPORTANTE: Limpiar máscara antes de enviar
La máscara guarda formato visual (`0412-8340975`), pero en BD se debe guardar solo dígitos (`04128340975`). **Siempre** limpiar antes del submit:
```js
$('#miFormulario').on('submit', function() {
$('#telefono').val($('#telefono').val().replace(/\D/g, ''));
});
```
O con `.cleanVal()` si se usa `jquery.mask`:
```js
$('#miFormulario').on('submit', function() {
$('#telefono').val($('#telefono').cleanVal());
});
```
#### Ejemplo completo en vista
```blade
@push('scripts')
<script>
$(function() {
// Máscaras
$('#telefono').mask('0000-0000000', { placeholder: '0412-0000000' });
// Validación
$('#formCliente').validate({
rules: {
telefono: { phoneVE: true }
}
});
// Limpiar antes de enviar
$('#formCliente').on('submit', function() {
$('#telefono').val($('#telefono').cleanVal());
});
});
</script>
@endpush
```
### 16.7 Orden de campos en formularios de clientes
El orden correcto de los campos al crear/editar un cliente es:
1. **Cédula** (primer campo, obligatorio)
2. Nombre
3. Apellido
4. Email
5. Teléfono
6. Tipo de Cliente
7. Dirección
8. Ciudad
9. Código Postal
10. Notas
11. Estado (Activo/Inactivo)
### 16.8 Resumen de longitudes por campo (referencia rápida)
| Campo | `maxlength` HTML | `max` Backend | `min` Backend | `required` |
|-------|-------------------|---------------|---------------|------------|
| Cédula | 12 | `digits_between:6,12` | 6 | Sí |
| Nombre | 100 | 100 | 2 | Sí |
| Apellido | 100 | 100 | 2 | Sí |
| Email | 255 | 255 | — | Sí |
| Teléfono | 15 | 15 | 7 | No |
| Dirección | 255 | 255 | 5 | No |
| Ciudad | 80 | 80 | — | No |
| Código Postal | 10 | 10 | — | No |
| Notas | 500 | 500 | — | No |
---
## 17. CSS Flexbox — Reglas de uso
Flexbox se usa para alinear y distribuir elementos dentro de un contenedor. **Siempre preferir Flexbox antes que hacks con `float`, `position: absolute` o `margin` negativo.**
### 17.1 Dirección del flex container
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **1. `flex-row`** (default) | `d-flex` | `display:flex; flex-direction:row;` | Elementos en línea horizontal, de izquierda a derecha. **Es el default, no necesita clase extra.** |
| **2. `flex-row-reverse`** | `d-flex flex-row-reverse` | `flex-direction:row-reverse;` | Elementos en línea horizontal, de derecha a izquierda. Útil para alinear acciones a la derecha manteniendo el orden DOM. |
| **3. `flex-column`** | `d-flex flex-column` | `flex-direction:column;` | Elementos apilados verticalmente. Para formularios, tarjetas, listas verticales. |
```html
<!-- 1. Row (default) — elementos en línea -->
<div class="d-flex">
<span>Izquierda</span>
<span>Derecha</span>
</div>
<!-- 2. Row reverse — acciones a la derecha -->
<div class="d-flex flex-row-reverse">
<button>Cancelar</button>
<button>Guardar</button>
</div>
<!-- 3. Column — apilado vertical -->
<div class="d-flex flex-column">
<label>Nombre</label>
<input type="text">
</div>
```
### 17.2 Justificación (eje principal — horizontal en row)
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **4. `justify-content-*`** | `justify-content-between` | `justify-content:space-between;` | Distribuir espacio entre elementos: primero a la izquierda, último a la derecha. **El más usado.** |
| | `justify-content-start` | `justify-content:flex-start;` | Todos alineados al inicio (izquierda). |
| | `justify-content-end` | `justify-content:flex-end;` | Todos alineados al final (derecha). |
| | `justify-content-center` | `justify-content:center;` | Todos centrados. |
| | `justify-content-around` | `justify-content:space-around;` | Espacio uniforme alrededor de cada elemento. |
| | `justify-content-evenly` | `justify-content:space-evenly;` | Espacio completamente uniforme. |
```html
<!-- 4. Justify — barra de acciones: título izquierda, botones derecha -->
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Título</h5>
<div class="d-flex gap-2">
<button>Cancelar</button>
<button>Guardar</button>
</div>
</div>
```
### 17.3 Alineación (eje transversal — vertical en row)
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **5. `align-items-center`** | `align-items-center` | `align-items:center;` | Centrar elementos verticalmente dentro del flex container. **El más usado para alinear íconos con texto, botones con labels, tarjetas en fila.** |
| | `align-items-start` | `align-items:flex-start;` | Todos arriba. |
| | `align-items-end` | `align-items:flex-end;` | Todos abajo. |
| | `align-items-stretch` | `align-items:stretch;` | Estirar para igualar altura (default). |
| | `align-self-center` | `align-self:center;` | Centrar solo un hijo específico. |
```html
<!-- 5. Align items center — ícono alineado con texto -->
<div class="d-flex align-items-center gap-2">
<i class="fas fa-user"></i>
<span>Nombre del cliente</span>
</div>
<!-- Combinación más común: header de tarjeta -->
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-cog me-2"></i>Configuración</h5>
<button class="btn btn-sm btn-primary">Guardar</button>
</div>
```
### 17.4_GAP_—_espaciado_entre_elementos
| Clase Bootstrap | CSS nativo | Descripción |
|-----------------|------------|-------------|
| `gap-1` | `gap: 0.25rem;` | 4px |
| `gap-2` | `gap: 0.5rem;` | 8px |
| `gap-3` | `gap: 1rem;` | 16px |
| `gap-4` | `gap: 1.5rem;` | 24px |
| `gap-5` | `gap: 2rem;` | 32px |
> **Regla:** Usar `gap-*` en vez de `margin` en hijos para espaciar elementos flex. Es más limpio y predecible.
### 17.5 Combinaciones Flexbox más comunes en el proyecto
```html
<!-- Header de tarjeta: título + botones -->
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Título</h5>
<div class="d-flex gap-2">Botones...</div>
</div>
<!-- Fila de formulario: 2 campos lado a lado -->
<div class="row">
<div class="col-md-6 mb-3">Campo 1</div>
<div class="col-md-6 mb-3">Campo 2</div>
</div>
<!-- Badge + texto alineados -->
<div class="d-flex align-items-center gap-2">
<span class="badge bg-success">Activo</span>
<span class="text-muted small">Desde 01/01/2026</span>
</div>
<!-- Botones apilados verticalmente (sidebar) -->
<div class="d-flex flex-column gap-2">
<a class="btn btn-primary">Opción 1</a>
<a class="btn btn-outline-secondary">Opción 2</a>
</div>
<!-- Acciones a la derecha, contenido a la izquierda -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h4 class="mb-0">Título</h4>
<small class="text-muted">Subtítulo</small>
</div>
<a href="#" class="btn btn-primary">Acción</a>
</div>
```
### 17.6 Regla de decisión: ¿cuándo usar Flexbox?
| Situación | Solución |
|-----------|----------|
| 2+ elementos en fila, alineados verticalmente | `d-flex align-items-center` |
| Header con título a la izquierda, botones a la derecha | `d-flex justify-content-between align-items-center` |
| Elementos apilados verticalmente | `d-flex flex-column` |
| Botones/acciones en fila con espacio entre ellos | `d-flex gap-2` |
| Ícono junto a texto (checkbox, badges, labels) | `d-flex align-items-center gap-2` |
| Invertir orden visual sin cambiar DOM | `d-flex flex-row-reverse` |
---
## 18. Navegación: botones Cancelar y Volver
### 18.1 Regla general
**Todo botón "Cancelar" o "Volver" debe regresar a la página anterior real del usuario**, no a una ruta fija. Se usa `url()->previous()` de Laravel.
```php
// ❌ MAL — ruta fija, pierde el contexto
<a href="{{ route('trabajos.index') }}">Cancelar</a>
// ✅ BIEN — regresa de donde vino
<a href="{{ url()->previous() }}">Cancelar</a>
```
### 18.2 Tipos de botones de navegación
| Botón | Comportamiento | Ejemplo |
|-------|----------------|---------|
| **"Cancelar"** (en formularios) | `url()->previous()` | Cancelar creación/edición de factura, trabajo, material, cliente |
| **"Volver"** (en vistas show) | `url()->previous()` | Volver desde vista detalle de trabajo, cliente, material |
| **"Editar"** (navegación directa) | `route('entidad.edit', $id)` | Botón que lleva al formulario de edición |
| **"Ver"** (navegación directa) | `route('entidad.show', $id)` | Botón que lleva a la vista detalle |
### 18.3 Formularios que deben usar `url()->previous()`
| Vista | Botón | Antes (❌) | Ahora (✅) |
|-------|-------|-----------|-----------|
| `facturas/create` | Cancelar | `route('facturas.index')` | `url()->previous()` |
| `facturas/edit` | Cancelar | `route('facturas.show')` | `url()->previous()` |
| `trabajos/create` | Cancelar | `route('trabajos.index')` | `url()->previous()` |
| `trabajos/edit` | Volver | `route('trabajos.show')` | `url()->previous()` |
| `materiales/create` | Cancelar | `route('materiales.index')` | `url()->previous()` |
| `materiales/edit` | Cancelar | `route('materiales.index')` | `url()->previous()` |
| `clientes/create` | Volver / Cancelar | `route('clientes.index')` | `url()->previous()` |
| `clientes/edit` | Volver | `route('clientes.index')` | `url()->previous()` |
| `admin/users/create` | Volver | `route('admin.users.index')` | `url()->previous()` |
| `admin/users/edit` | Volver | `route('admin.users.index')` | `url()->previous()` |
### 18.4 Vistas show que deben tener botón "Volver"
Toda vista `show.blade.php` **debe** incluir un botón "Volver" con `url()->previous()` en el header:
```blade
<div class="btn-group">
<a href="{{ url()->previous() }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Volver
</a>
<a href="{{ route('entidad.edit', $entidad) }}" class="btn btn-secondary">
<i class="fas fa-edit me-2"></i>Editar
</a>
</div>
```
### 18.5 Eliminar botones redundantes
**No duplicar información.** Si una vista ya muestra todos los datos de una entidad (ej: `trabajos/cliente.blade.php` muestra nombre, email, teléfono, ciudad del cliente), no agregar un botón "Ficha" que lleve a otra vista con la misma información.
| Vista | Botón eliminado | Razón |
|-------|-----------------|-------|
| `trabajos/cliente.blade.php` | "Ficha" (→ `clientes.show`) | Redundante: los datos del cliente ya se muestran en la tarjeta superior |
---
## 19. Auditoría de acciones de usuarios
> **Regla obligatoria:** **TODAS las acciones de TODOS los usuarios deben quedar registradas** en un módulo de **auditoría** dentro del sistema. Este registro sirve para recordar y llevar un **control minucioso** de todo lo que se haga en el sistema.
### 19.1 Qué se registra
Cada acción relevante (crear, leer, editar, eliminar, emitir/cancelar, iniciar/cerrar sesión, cambios de estado, etc.) debe guardar al menos:
- **Usuario** que realizó la acción (o `system`/`guest` si es pública).
- **Acción** (crear, actualizar, eliminar, emitir, login, logout, etc.).
- **Entidad/Recurso** afectado (ej: `factura`, `trabajo`, `cliente`, `usuario`) y su **ID**.
- **Descripción** legible de lo que se hizo.
- **Datos previos/cambios** relevantes (dato anterior → dato nuevo) cuando aplique.
- **Fecha y hora** exactas del evento (timestamp).
- **IP de la máquina** desde la que se realizó la acción (`request()->ip()`).
### 19.2 Acceso restringido al administrador
- El **módulo de auditoría SOLO lo puede ver el usuario administrador** (`role:admin`).
- Rutas del módulo bajo middleware `role:admin` (o equivalente de jerarquía).
- Debe permitir **búsqueda y filtrado** por usuario, acción, entidad, rango de fechas e IP.
### 19.3 Cómo implementarlo
- Un **modelo** `Auditoria` (+ migración y tabla `auditorias`) con los campos del punto 19.1.
- Un **servicio** centralizado: `AuditoriaService::registrar($accion, $entidad, $id, $descripcion, $datosAntes, $datosDespues)` que inyecte automáticamente `user_id`, `ip` y timestamp.
- Registrar las acciones en controladores/servicios como parte del flujo normal (no en vistas ni consultas fuera de servicios).
- La tabla `auditorias` **no debe tener soft deletes ni edición**: es un registro inmutable de seguridad; solo se consulta (nunca se elimina vía la app).
- Índices para el admin: `user_id`, `entidad`, `created_at` (y compuesto `entidad + created_at`).
- **Seedear** registros de ejemplo (Seeder `AuditoriasSeeder`) para probar la vista del admin.
### 19.4 Vista de administración
- Página `auditorias/index` (solo admin) con eventos ordenados por fecha descendente.
- Filtros: usuario, acción, entidad, IP y rango de fechas.
- Mostrar claramente: fecha/hora, usuario, acción, entidad + ID, IP y descripción.
---
## 20. Ahorro de Tokens
**Reglas Críticas (anti-loop/verbose):**
- Si repites acción/lectura/comando igual 2ª vez consecutiva → para.
- Si falla → 2 intentos máx, luego reportar y detener.
- No repitas texto ya emitido.
- Antes de cada tool: `<[ACCIÓN]: qué/y por qué>`.
- Al responder: éxito o error.
- Si piden código → solo bloque, con comentario `// LANG` inicial, sin explicación.
---
## 21. Backups: verificación de datos
**Regla general (de obligado cumplimiento):** al crear un backup **siempre** se debe verificar que TODOS los datos están siendo respaldados correctamente. No basta con que el ZIP se genere.
Qué se verifica en cada backup (`BackupService::crearBackup`):
- **Cobertura de tablas:** todas las tablas de `public` (excepto técnicas: `migrations`, `sessions`, `cache*`, `jobs*`, `password_reset_tokens`, `personal_access_tokens`) deben estar en `BackupSeederGenerator::$tables` y generar su seeder si tienen datos.
- **Archivos adjuntos:** contar archivos reales en `storage/app/public/{trabajos,comprobantes,logo}` y comparar con los copiados al ZIP. Cualquier faltante → backup **fallido**.
- **Logo:** si hay una configuración `empresa_logo`, el archivo debe existir en `archivos/logo/` dentro del ZIP.
Reglas derivadas:
- Si una tabla nueva se agrega al esquema y contiene datos → debe incorporarse a `$tables` y a `obtenerNombreClase()`, o la verificación lo reportará como pendiente.
- Al restaurar, tanto los seeders como los archivos adjuntos (fotos, comprobantes, logo) se devuelven a su ubicación.
- Toda nueva carpeta de archivos que guarde datos del usuario debe agregarse al backup (o la verificación no será completa).
---
## 22. Tareas pendientes obligatorias
**Regla general (de obligado cumplimiento):** **NO** se puede iniciar ni continuar ninguna tarea nueva o solicitud del usuario si existen tareas pendientes del flujo de trabajo actual. Las tareas pendientes deben finalizarse **obligatoriamente** antes de poder avanzar.
Reglas derivadas:
- Al detectar tareas pendientes (por ejemplo, en el `todowrite` o en la lista de pendientes de una sesión), se deben **completar todas** antes de comenzar otra cosa.
- Si una tarea queda sin terminar por un bloqueo, se debe: reportar el bloqueo, dejar el estado como `in_progress` y agregar una tarea de seguimiento explícita. **No** se continúa con trabajo no relacionado.
- Antes de cerrar una sesión o de responder a una nueva petición, verificar que no queden tareas `pending` o `in_progress` sin resolver.
- La finalización de una tarea implica **verificarla** (compilar vistas, `php -l`, probar el flujo por tinker/HTTP según aplique) antes de marcarla como `completed`.
- Si el usuario solicita algo nuevo mientras aún hay pendientes, **no** cambiar de contexto: finalizar primero lo pendiente y luego atender lo nuevo.
---
---
## 23. API de tasa de cambio (VES/USD)
**Regla general (de obligado cumplimiento):** para cualquier proyecto que necesite la tasa de cambio del Bolívar (VES) por Dólar (USD) en Venezuela, usar la API **`https://ve.dolarapi.com/v1/dolares`**.
**Endpoint y formato de respuesta:**
- URL: `https://ve.dolarapi.com/v1/dolares`
- Devuelve un **array de objetos**, uno por cada fuente:
```json
[
{ "moneda": "USD", "fuente": "oficial", "nombre": "Dólar", "compra": null, "venta": null, "promedio": 801.1752, "fechaActualizacion": "2026-09-02T00:00:00-04:00" },
{ "moneda": "USD", "fuente": "paralelo", "nombre": "Paralelo", ... }
]
```
- La tasa oficial (BCV) es el elemento cuya `fuente === "oficial"`, y el valor a usar es su campo **`promedio`**.
**Regla de decimales (obligatoria):** en cualquier proyecto donde se muestre o use esta información, el valor de la tasa **debe presentarse con 2 decimales** después del punto (p. ej. `801.18`, no `801.1752`). Aplicar `round($valor, 2)` en backend y `toFixed(2)` en el frontend/JS.
**Implementación sugerida (Laravel):**
- Guardar la URL en `.env` como `TASA_API_BCV_URL=https://ve.dolarapi.com/v1/dolares`.
- En el servicio de tasa: consumir el endpoint, recorrer el array, localizar el elemento con `fuente === 'oficial'`, usar su `promedio`, redondear a 2 decimales y devolverlo.
- Tener un fallback: si la API no responde, usar la última tasa guardada en configuración o un valor fallback en `.env`.
- El campo input del formulario debe usar `step="0.01"` y rellenarse con `toFixed(2)`.
---
---
## 24. Orden de listados por fecha de creación (más recientes primero)
**Regla general (de obligado cumplimiento):** en las **tablas/índices de listado** de todos los módulos (clientes, proveedores, materiales, transportistas, facturas, pagos, trabajos, etc.), los registros deben aparecer **por orden de creación, los más recientes primero** (`orderBy('created_at', 'desc')`, o la columna de fecha equivalente del módulo en desc).
**Lugares que deben cumplir la regla (obligatorio en todos):**
- Índice de clientes (`clientes.index`).
- Índice de proveedores, materiales, transportistas.
- Índices de facturas, pagos, trabajos (por su fecha/fecha de emisión en desc).
- Listados de clientes en reportes (si aplica, usar el mismo criterio).
**Excepción — selects/dropdowns (NO cambiar):** al elegir un cliente (o material/proveedor/transportista) dentro de un **formulario** (trabajos, pagos, entregas, facturas, traslados), el listado del `<select>` **sí** se mantiene **ordenado alfabéticamente por nombre** (`orderBy('nombre')`) para facilitar la búsqueda manual del usuario.
**Implementación (Laravel):**
- En los índices: `->orderBy('created_at', 'desc')` (o `orderByDesc('fecha_*')` según el módulo).
- En los selects de formularios: `->orderBy('nombre')` (sin cambios).
- **No** usar `orderBy('nombre')` en los índices de listado.
- Los selects/dropdowns SÍ usan `orderBy('nombre')`.
## 24a. Reutilización del formulario de cliente
**Regla (de obligado cumplimiento):** **no duplicar formularios**. El formulario de creación/edición de cliente es único (`clientes.create`). Cuando en otro módulo (ej. formulario de nuevo trabajo) se necesite registrar un cliente, se debe **enlazar** a `clientes.create` y, tras guardar, **redirigir de vuelta** al lugar desde donde se accedió:
- El botón "+Nuevo" de cliente en `trabajos.create` es un enlace a `route('clientes.create', ['redirect' => 'trabajos.create'])`.
- `ClienteController@store` redirige a `request('redirect')` si viene con ese parámetro.
- **No** crear un segundo formulario / modal de cliente por módulo.
---
---
La frase traducida a humano: Eloquent = para cosas que tu app guarda, cambia y que tienen reglas Query Builder = para cosas que tu app solo lee, cuenta o muestra Eso es todo. No es más misterioso que eso. 👍
# Instalar pandoc sudo apt-get install pandoc # Linux brew install pandoc # Mac # Windows: descargar de pandoc.org # Convertir Markdown a PDF pandoc guia.md -o guia.pdf --pdf-engine=xelatex # Con formato más bonito pandoc guia.md -o guia.pdf --template=eisvogel
<body>
<div class="footer-banner">
<img src="https://static.gwvkyk.com/media/1830380bf3c86410b627d.png">
</div>
<div class="footer-box">
<div>
<p class="footer-title">BET365PNG – Hottest Online Casino and Pokies in Papua New Guinea</p>
<p class="footer-paragraph">
Looking for more ways to play online in Papua New Guinea?
<a href="https://bet365png.com/" class="text-link">BET365PNG</a>
brings pokies, live casino games, sports betting and exciting promotions together in one convenient platform. Enjoy popular games and entertainment designed for players across Papua New Guinea.
</p>
<p class="footer-paragraph">
BET365PNG is accessible on mobile, tablet and desktop, with PGK promotions, local payment options and exclusive VIP rewards. Play anywhere with our
<a href="https://bet365png.com/" class="text-link">dedicated online betting app</a>
and access pokies, live casino games and sports betting with ease. Visit the
<a href="https://bet365png.com/" class="text-link">official BET365PNG website</a>
to explore the latest games and offers.
</p>
<p class="footer-paragraph">
Don’t miss the action —
<a href="https://bet365png.com/" class="text-link">visit BET365PNG today</a>
to explore popular games, promotions and exclusive member rewards.
</p>
<div class="footer-box-sub">
<h2 class="h2-footer-title ">Why Bet at Bet365Png Online Live Casino</h2>
<p class="footer-paragraph">BET365PNG brings the atmosphere of live casino gaming closer to players across Papua New Guinea.</p>
<p class="footer-paragraph">Designed with local players in mind, the platform makes it simple to access live casino entertainment from your phone, tablet, or computer.</p>
<p class="footer-paragraph">Players can enjoy an engaging online casino experience with convenient access across different devices.</p>
<h3 class="h3-footer-title"><ol class="number-list"><li>Live Casino and More, All in One Place</li></ol></h3>
<p class="footer-paragraph">BET365PNG is designed for players who want variety without jumping between different platforms.</p>
<p class="footer-paragraph">Players can explore popular live casino favourites including:</p>
<ul class="disc-list">
<li><b>Live Blackjack: </b>Take on the dealer in real time and play toward the classic 21.</li>
<li><b>Live Roulette: </b>Follow every spin live and choose from a variety of betting options.</li>
<li><b>Live Baccarat: </b>Move quickly between Player, Banker and Tie bets at live tables.</li>
<li><b>Live Game Shows: </b>Enjoy interactive casino-style entertainment hosted by live presenters.</li>
</ul>
<p class="footer-paragraph">What sets BET365PNG apart is how easily players can move from one experience to another. Start with a live baccarat table, switch to roulette, or explore pokies without leaving the same platform.</p>
<h3 class="h3-footer-title"><ol start="2" class="number-list"><li>More Pokies, Better RTP, More Choice for Players</li></ol></h3>
<p class="footer-paragraph">RTP (Return to Players). Itself meaning that simply by putting the player win on a higher ground and player get higher win rates to player.
Higher Long Term Return for player to win and a better starting point compared with a game that has a lower RTP</p>
<ul class="disc-list">
<li><b>Classic Pokies: </b>Traditional slot machines with simple gameplay and nostalgic themes.</li>
<li><b>Video Slots: </b>Modern pokies packed with bonus features, free spins, and interactive gameplay.</li>
<li><b>Progressive Jackpots: </b>A chance to land life-changing payouts as the prize pool increases with every bet.</li>
<li><b>Megaways Pokies: </b>Thousands of ways to win on a single spin with unique game mechanics.</li>
</ul>
<p class="footer-paragraph">With high RTP games, players enjoy better payout percentages, maximising their
winning potential while experiencing thrilling gameplay. Whether you prefer simple three-reel classics or
feature-rich video pokies, Bet365Png has pokies for every type of player.</p>
<h3 class="h3-footer-title"><ol start="3" class="number-list"><li>Bet On Your Favourite — Play Live, Win Live </li></ol></h3>
<p class="footer-paragraph">BET365PNG gives sports fans an easy way to bet on popular local and international events. From football and basketball to rugby and tennis, players can find different sports, matches and betting markets in one place.</p>
<ul class="disc-list">
<li><b>Pre-Match and Live Betting: </b>Place your bet before the game starts or bet while the match is happening.</li>
<li><b>Competitive Odds: </b>Compare different markets and choose the odds that suit your bet.</li>
<li><b>More Bet Types, More Opportunity: </b>Choose from single bets, accumulators, over/under markets and other options.</li>
<li><b>Live Score Updates & Statistics: </b>Follow the latest scores and match information while you play.</li>
</ul>
<p class="footer-paragraph">Bet365Png makes sports betting simple, exciting, and rewarding, catering to both
casual and experienced bettors alike.</p>
<h3 class="h3-footer-title"><ol start="4" class="number-list"><li>Fast Deposits. Smooth Withdrawals.</li></ol></h3>
<p class="footer-paragraph">BET365PNG keeps payments simple, so players can spend more time enjoying the games and less time dealing with complicated transactions.</p>
<ul class="disc-list">
<li><b>Fast Deposits: </b>Add funds quickly and get straight into your favourite pokies, live casino games or sports markets.</li>
<li><b>Smooth Withdrawals: </b>A straightforward cash-out process makes it easier to access your funds when you are ready.</li>
<li><b>Convenient Payment Options: </b>ET365PNG provides practical payment choices designed to make deposits and withdrawals easier for players.</li>
<li><b>Simple from Start to Finish: </b>From adding funds to requesting a withdrawal, the process is designed to be clear and easy to follow.</li>
</ul>
<p class="footer-paragraph">Bet365Png makes sure that you spend less time waiting and more time playing,
with seamless banking solutions tailored for convenience.</p>
<h3 class="h3-footer-title"><ol start="5" class="number-list"><li>Take Your Gaming to the Next Level with the BET365PNG App</li></ol></h3>
<p class="footer-paragraph">Enjoy your favourite BET365PNG games wherever you are. The BET365PNG App gives players quick and easy access to pokies, live casino games and sports betting directly from their mobile device.</p>
<ul class="disc-list">
<li><b>Optimised for Mobile: </b>The app delivers smooth performance on all devices, whether you're using Android or iOS.</li>
<li><b>Easy Navigation: </b>Simple navigation helps you find your favourite games and betting options faster.</li>
<li><b>Quick Access: </b>Log in and start playing without going through a complicated process.</li>
<li><b>Live Sports & Betting: </b>Follow live sports action and place bets while the game is happening.</li>
</ul>
<p class="footer-paragraph">With the BET365PNG App, your favourite games are always within reach. Play, bet and explore anytime, all from one convenient app.</p>
<h3 class="h3-footer-title"><ol start="6" class="number-list"><li>Play with Confidence at BET365PNG, Safer & Secure</li></ol></h3>
<p class="footer-paragraph">BET365PNG focuses on giving players a safer and more reliable place to enjoy pokies, live casino games and sports betting.</p>
<ul class="disc-list">
<li><b>Regulated Payment: </b>Player deposits, withdrawals and account information are handled with security in mind.</li>
<li><b>Protected Transactions: </b>Security measures help keep personal and payment details private.</li>
<li><b>Fair Gameplay: </b>Games are designed to provide clear rules and fair results, so players know what to expect.</li>
<li><b>Responsible Gaming Features: </b>BET365PNG encourages players to stay in control, play within their limits and enjoy gaming responsibly.</li>
</ul>
<p class="footer-paragraph">From creating an account to placing a bet or making a withdrawal, BET365PNG aims to keep the experience simple, secure and comfortable from start to finish.</p>
<h3 class="h3-footer-title"><ol start="7" class="number-list"><li>Exclusive Casino Rewards and Promotions</li></ol></h3>
<p class="footer-paragraph">BET365PNG gives more rewards, more reasons for player to play, daily bonuses, free credit offers and special promotions across casino and sports.</p>
<h4 class="h4-footer-title ">Welcome Special Rewards</h4>
<ul class="disc-list">
<li><b>Welcome Bonus: </b>Get a 100% bonus with no maximum withdrawal limit when you make your first deposit.</li>
<li><b>1st Daily Deposit Bonus: </b>Receive a 20% bonus on your first deposit each day.</li>
<li><b>Unlimited Bonus 5% & 10%: </b>Enjoy unlimited 5% and 10% bonuses on eligible deposits.</li>
</ul>
<h4 class="h4-footer-title ">Free Credit & Rewards</h4>
<p class="footer-paragraph">Unlock extra rewards through fun and simple activities:</p>
<ul class="disc-list">
<li><b>Smash the Golden Egg: </b>Deposit PGK1 to earn 1 point towards exclusive rewards.</li>
<li><b>Register and Get PGK20: </b>Sign up and receive a PGK20 free credit to start playing.</li>
<li><b>Daily App Download Bonus: </b>Download the Bet365Png app and claim free PGK rewards daily.</li>
<li><b>Referral Bonus: </b>Invite friends and earn PGK10 for every successful referral.</li>
<li><b>Facebook Share Bonus: </b>Share Bet365Png on Facebook and receive PGK7 in free credit.</li>
</ul>
<h4 class="h4-footer-title ">Live Casino & Sports Promotions</h4>
<ul class="disc-list">
<li><b>Live Casino Cashback: </b>Receive cashback on losses while playing live dealer games.</li>
<li><b>Sports Betting Boosts: </b>Take advantage of odds boosts and special promotions on major sporting events.</li>
</ul>
<h4 class="h4-footer-title ">VIP Loyalty Bonus</h4>
<p class="footer-paragraph">Bet365Png rewards loyal players with exclusive VIP bonuses based on their level:</p>
<ul class="disc-list">
<li><b>VIP Level 1: </b>PGK38</li>
<li><b>VIP Level 2: </b>PGK98</li>
<li><b>VIP Level 3: </b>PGK288</li>
<li><b>VIP Level 4: </b>PGK588</li>
<li><b>VIP Level 5: </b>PGK888</li>
</ul>
<p class="footer-paragraph">With regular casino promotions and exclusive VIP rewards, Bet365Png ensures that
every player gets the most value from their gaming experience.
<a href="https://bet365png.com/" class="text-link">Join today</a> and start claiming your bonuses!</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title ">Discover Top Online Casino Games only at Bet365Png</h2>
<p class="footer-paragraph">BET365PNG brings together a wide range of online casino games in one place, giving players more ways to enjoy their favourite type of gameplay. From pokies and live casino tables to sports betting, players can easily switch between different options without leaving the platform.
With smooth mobile access, simple navigation and a broad choice of games, BET365PNG makes it easy to find something that matches your playing style.
Explore some of the most popular gaming options available at BET365PNG.
</p>
<p class="footer-paragraph">Explore the best online casino games at Bet365Png, including:</p>
<h3 class="h3-footer-title">Pokies</h3>
<p class="footer-paragraph">Pokies are a favourite among players, offering high RTP rates, thrilling bonus
features, and diverse themes. From classic three-reel Pokie to modern video pokies with wilds, scatters, and free
spins, every spin brings the chance to land big wins. Progressive jackpot slots also provide massive payout
opportunities for those chasing life-changing prizes.</p>
<h4 class="h4-footer-title">Trusted Pokies Game Providers at Bet365Png</h4>
<h5 class="h5-footer-title ">MEGAH5</h5>
<p class="footer-paragraph">MegaH5 is a reputable slot game provider with a strong presence in both B2B and
B2C sectors. Their advanced backend system allows operators to verify game transactions and customise
betting options, ensuring a smooth and flexible gaming experience.</p>
<p class="footer-paragraph">With multi-currency support and cross-device compatibility, MegaH5 delivers an
accessible and engaging platform for players worldwide. Their games feature stunning visuals, immersive sound
design, and exciting gameplay mechanics, creating a captivating experience.</p>
<p class="footer-paragraph">MegaH5 also enhances player engagement by introducing super jackpots and
high-payout segments, offering more opportunities to win big. With a consistent release schedule and a strong
brand identity, they continue to be a trusted choice for operators and an exciting destination for players.</p>
<h5 class="h5-footer-title">Imperium Games</h5>
<p class="footer-paragraph">Imperium-Games is a forward-thinking iGaming provider, delivering cutting-edge
and customisable gaming solutions for operators worldwide. Since 2014, they have been committed to redefining
online gaming with innovative and reliable technology that enhances both operator efficiency and player
engagement.</p>
<p class="footer-paragraph">Their diverse portfolio includes casino games, live dealer solutions, VLT platforms,
and mobile gaming software, all designed to provide seamless, high-quality experiences. With robust API
integration, operators can easily incorporate Imperium-Games’ advanced software into their platforms, ensuring
smooth gameplay across multiple devices.</p>
<p class="footer-paragraph">Imperium-Games stands out for its commitment to innovation, tailored solutions,
and long-term partnerships. By combining creativity with cutting-edge technology, they continue to push industry
boundaries, offering gaming experiences that set new standards in the iGaming world.</p>
<h5 class="h5-footer-title">V Power Casino</h5>
<p class="footer-paragraph">V Power Casino, now known as V Blink, has been a trusted gaming software
provider since 2018, offering some of the fastest services and best rates in the industry. Their platform, Vblink777,
is home to over 100 games, including pokies, table games, fishing, and arcade games, ensuring a diverse and
engaging experience for all players.</p>
<p class="footer-paragraph">Compatible with Android, iOS, and PC, V Blink provides seamless accessibility
across multiple devices. The platform stands out for its easy registration process, free spin wheels, exclusive
bonuses, and loyalty rewards, making it a go-to choice for both casual and serious players.</p>
<p class="footer-paragraph">Security is a top priority, with SSL encryption and end-to-end transaction protection,
ensuring that player data and funds remain safe. With various game modes, real-money betting options, and
frequent promotions, V Blink offers an immersive and rewarding online casino experience.</p>
<h5 class="h5-footer-title">JILI Games</h5>
<p class="footer-paragraph">JILI Gaming is a leading online casino game provider known for its commitment to
innovation and excellence. With a team of experienced developers, JILI creates high-quality video slots, bingo,
table games, and fishing games, constantly pushing the boundaries of gaming entertainment.</p>
<p class="footer-paragraph">Their portfolio includes over 100 HTML5 games, ensuring seamless cross-platform
play. With support for 50+ currencies and 12+ languages, JILI caters to a global audience. The platform also offers
innovative mechanics, easy integration for operators, and exciting features like tournaments and jackpots, making
it a top choice for both players and gaming platforms worldwide.</p>
<h5 class="h5-footer-title">Red Tiger</h5>
<p class="footer-paragraph">Red Tiger is a renowned casino game developer, established in 2014 by industry
veterans with a passion for creating top-tier slot games. With a team of mathematicians, designers, developers,
and gaming experts, Red Tiger is dedicated to enhancing the player experience through engaging and innovative
game mechanics.</p>
<p class="footer-paragraph">Committed to regulated markets, Red Tiger holds multiple gaming licenses across
jurisdictions, including the UK, Malta, Alderney, and Gibraltar. As part of the Evolution Gaming Group, the
company continues to push the boundaries of online casino entertainment, partnering with top-tier operators
worldwide and earning industry recognition for its high-quality games.</p>
<h5 class="h5-footer-title">Pragmatic Play</h5>
<p class="footer-paragraph">Pragmatic Play is a leading provider of innovative digital casino games, with a
diverse portfolio that includes award-winning slots, live casino, bingo, virtual sports, and sportsbook options.
Headquartered in Gibraltar and led by CEO Julian Jarvis, the company offers a multi-product experience through
a single API, making it easy for operators to integrate and deliver top-quality content.</p>
<p class="footer-paragraph">Certified and licensed in over 40 jurisdictions, Pragmatic Play delivers content that
is available in 33 languages and all major currencies. With a strong focus on responsible gambling, the company
maintains a commitment to fair play, compliance with regulatory standards, and player protection, ensuring an
immersive and socially responsible gaming experience for players worldwide.</p>
<h5 class="h5-footer-title">Fastspin</h5>
<p class="footer-paragraph">Fastspin is a game studio focused on creating unique and culturally resonant
games that appeal to players worldwide. The company prides itself on offering innovative game features and
superior interfaces, ensuring players enjoy seamless experiences across various mobile devices. Fastspin’s
portfolio includes games designed for mobile and tablet devices with stunning graphics and immersive sounds,
making it accessible and enjoyable anytime, anywhere.</p>
<p class="footer-paragraph">Certified and licensed in over 20 jurisdictions, Fastspin’s games are available in 11
languages and all currencies. The studio is committed to responsible gaming, providing partners with the
necessary tools to maintain a safe, enjoyable environment for players. Fastspin ensures all games are
compliant with regulations, ensuring fairness and supporting healthy playing habits.</p>
<h5 class="h5-footer-title">Playtech</h5>
<p class="footer-paragraph">Founded in 1999, Playtech is a global leader in platform, content, and services for
the online gambling industry. The company operates in over 20 countries, with around 7,900 employees, and
serves more than 180 licensees across 40+ regulated jurisdictions.</p>
<p class="footer-paragraph">The company’s strategy aims for revenue growth, margin expansion, and
generating long-term stakeholder value. Core values like integrity, innovation, excellence, and performance
drive its operations, with proprietary technology powering a comprehensive range of platform, content, and
services. Playtech’s data-driven approach enhances customer experiences through intelligent platform features.</p>
<p class="footer-paragraph">In addition to innovation and sustainability, Playtech focuses on delivering
omnichannel gaming experiences. Through its B2C division, which includes Snaitech, the company operates in
retail and online betting markets across Italy, Austria, and Germany. The company’s governance ensures
long-term sustainability and shareholder fairness, maintaining strong industry leadership.</p>
<h5 class="h5-footer-title">JDB Gaming</h5>
<p class="footer-paragraph">JDB Gaming is a leading online game developer, particularly renowned for its Fish
Shooting Games, and has gained significant popularity in the online casino industry. Known for its extensive
game portfolio, JDB offers a variety of gaming options including slots, arcade, bingo, card games, and more,
catering to diverse player preferences.</p>
<p class="footer-paragraph">The company is committed to delivering top-quality products with unique features
such as localised gameplay, where cultural elements are integrated into games to resonate with players
worldwide. JDB also provides secure API integration and uses big data analysis to enhance the gaming
experience, ensuring better player engagement and loyalty.</p>
<p class="footer-paragraph">JDB’s mission is to innovate, challenge, and evolve, consistently pushing the
boundaries of gaming development. It offers 24/7 technical support to its partners, assisting with stable gaming
operations. The company also focuses on risk management, market prediction, and providing game
recommendations to optimise business growth. Its core values aim to provide seamless and personalised gaming
solutions, making JDB a trusted partner in the iGaming industry.</p>
<h5 class="h5-footer-title">Acewin</h5>
<p class="footer-paragraph">Ace Win is a professional game software development company with extensive
experience in creating a variety of gaming solutions. The company offers a diverse range of games, including
electronic table games, slots, casino games, fishing games, social games, and arcade games, available on both
PC and mobile platforms.</p>
<p class="footer-paragraph">Ace Win collaborates with both online platform providers and offline operators,
ensuring its games reach a broad audience. The company is committed to delivering quality gaming experiences
and invites partners to connect for potential collaboration opportunities.</p>
<h5 class="h5-footer-title">Mega888</h5>
<p class="footer-paragraph">Mega888 is a dynamic mobile online casino platform founded in 2018. Despite
being a relatively young company, it quickly gained recognition in the competitive gambling industry by offering a
wide variety of games and continuously improving its offerings. Mega888 allows players to enjoy a range of
casino games on their mobile devices, including slots, fishing games, arcade games, and table games, providing
flexibility in terms of time and location.</p>
<p class="footer-paragraph">The platform prides itself on fairness, ensuring that the win rates of games are 100%
random and not influenced by agents. Mega888 is known for its high win rates, which have earned positive
feedback from players. Initially launched in Malaysia, Mega888 has expanded its reach to several Southeast Asian
countries such as Indonesia, Thailand, and Vietnam, with plans for further global expansion.</p>
<h5 class="h5-footer-title">Mario Club</h5>
<p class="footer-paragraph">Gaming Mario, registered in the Republic of Seychelles, is a rapidly growing
developer and operator of mobile games in the iGaming industry. With over 60 talented staff members spread
across offices in Seychelles, Thailand, Taiwan, and Malaysia, the company stands as a leading force in the
gaming sector. The founder, Jim Tan, inspired by the iconic character Mario, established the company with the
vision of creating high-quality, innovative games.</p>
<p class="footer-paragraph">Gaming Mario operates the Mario Club, a unique AI-oriented iGaming community
that brings together millions of Asian gamers. It supports multiple languages and currencies, offering various
games such as slot games, fishing games, casino games, and arcade games. The company prides itself on
creating all its games in-house, ensuring a rigorous testing process and delivering engaging experiences to
players across mobile and desktop platforms.</p>
<p class="footer-paragraph">With over 10 years of experience in the iGaming industry, Gaming Mario offers a
comprehensive one-stop solution for operators, including a sophisticated back-office system, white-label game
solutions, and market-leading technology. Their games are HTML5-based, ensuring compatibility with 99.9% of
devices.</p>
<h5 class="h5-footer-title">BT Gaming</h5>
<p class="footer-paragraph">BT Gaming is a prominent game developer in Asia, renowned for its high-quality
gaming products and commitment to creating engaging experiences for players. With a team of experienced
professionals in programming, art, and numerical engineering, BT Gaming has grown to develop a wide array of
unique and competitive games.</p>
<p class="footer-paragraph">The company offers a diverse range of gaming options, including slots, fishing
games, table games, arcade games, and P2P battles. Their slot games are known for high-quality graphics,
various playing methods, and a high payout ratio, while their fishing games bring popular arcade machines into
the online space, ensuring players can enjoy exciting gameplay and the thrill of big rewards.</p>
<p class="footer-paragraph">BT Gaming focuses on customisation, offering personalized game platforms with
local features to attract customers more effectively. All their games are developed using HTML5 technology,
ensuring a rich, seamless experience across multiple platforms and devices. The company also provides
partners with detailed technical documentation for smooth API integration. With a stable RTP mechanism and fair
gameplay, BT Gaming aims to deliver games that captivate players while promoting long-term engagement.</p>
<h5 class="h5-footer-title">FA CHAI</h5>
<p class="footer-paragraph">FA CHAI (FC), established in 2019, is an innovative and dynamic game provider
that has made significant strides in the iGaming industry. With over 20 years of industry experience, FA CHAI
combines top-tier talent with a passion for creating unforgettable online gaming experiences. The company's
games are certified by GLI and are known for their exquisite sensory appeal, innovative gameplay, and unique
script designs.</p>
<p class="footer-paragraph">FA CHAI offers a diverse range of games, spanning over 50 titles in popular
categories such as classic and innovative slots, 3D fishing, arcade games (with their Coin Dozer being a notable
example), and engaging table games. Their games are celebrated for their ability to captivate players and lead
trends across the market.</p>
<p class="footer-paragraph">The company is committed to continuously evolving its offerings, with a steady
stream of game releases on a monthly and seasonal basis. FA CHAI also incorporates exclusive features such
as the "Event Function" and "Player Achievement System" to heighten player engagement and anticipation.</p>
<p class="footer-paragraph">Backed by a team of seasoned professionals in research and development, art
design, and customer support, FA CHAI is dedicated to providing tailor-made solutions and exceptional service
to clients. With a strong emphasis on innovation and market insight, FA CHAI is rapidly establishing itself as a
leader in the iGaming space.</p>
<h5 class="h5-footer-title">WOW Gaming</h5>
<p class="footer-paragraph">WOW Gaming is a pioneering iGaming supplier and platform aggregator based at
the intersection of East and West. The company specialises in creating immersive, localised games that appeal
to both local players and international audiences. WOW Gaming seamlessly integrates iconic titles with
distinctive Asian elements, ensuring adaptability across diverse markets and channels. Their goal is to transform
every game into a captivating interactive experience that entices players with characters and narratives they
can’t resist.</p>
<p class="footer-paragraph">Focused on the ASEAN and Indian markets, WOW Gaming incorporates local
culture and folklore into its offerings, delivering unparalleled gaming experiences that resonate deeply with
players. The company offers a suite of innovative features such as country-themed lobbies, multi-currency
wallets, real-money games with free-to-play elements, and personalised avatar systems.</p>
<p class="footer-paragraph">WOW Gaming's offerings include slots, table games, poker, and bingo, all
optimised for both mobile and web platforms. The studio is committed to seamless cross-platform compatibility,
ensuring players have an unforgettable experience no matter the device. With a focus on cutting-edge
technology and player engagement, WOW Gaming is poised to create remarkable gaming content for operators
and players alike.</p>
<h5 class="h5-footer-title">SpadeGaming</h5>
<p class="footer-paragraph">Spadegaming is an Asia-based game provider known for its innovation and
high-quality gaming products. The company combines global cultural elements with a strong focus on
Asian-themed games, delivering stunning graphics and exceptional sound effects across both mobile and
desktop platforms. Spadegaming has recently expanded its reach to the European market with the acquisition
of the Malta Gaming Authority (MGA) license, allowing it to offer its products to a wider audience.</p>
<p class="footer-paragraph">With a mission to develop enjoyable games that blend trends, creativity, and
innovation, Spadegaming is committed to transforming the gaming experience and lifestyle. The company’s
games are certified by BMM Testlabs and iTech Labs, ensuring full compliance with gaming jurisdiction
requirements. Spadegaming also offers a range of solutions for online casino operators, including account
management, customer service, and technical support.</p>
<p class="footer-paragraph">Dedicated to mobile development since 2013, Spadegaming’s products are
accessible on all tablets and smartphones, providing players with an immersive gaming experience anytime,
anywhere. The company partners with leading casino operators worldwide, including Softswiss, EveryMatrix,
and Betconstruct, to enhance their business performance with high-quality products and exceptional service.</p>
<p class="footer-paragraph">Spadegaming is also committed to responsible gaming, ensuring that all its games
meet the highest standards and are fully compliant with regulations in the jurisdictions it operates in. The
company provides tools to help its partners create a safe and enjoyable environment for players.</p>
<h5 class="h5-footer-title">ACE333</h5>
<p class="footer-paragraph">Ace333 is a leading provider of online casino games, offering a diverse selection
that includes arcade games, card games, table games, and slots. Designed to cater to the preferences of Asian
players, the games feature innovative bonuses and generous payouts.</p>
<p class="footer-paragraph">Since its inception, Ace333 has built a strong reputation for delivering high-quality,
engaging games. The company’s team of iGaming professionals understands the needs of online casino gamers
and strives to provide the best gaming experience possible.</p>
<p class="footer-paragraph">Based in Malaysia, Ace333 powers some of the most reputable online casinos
across Asia, particularly in Malaysia and Thailand. Their game library includes popular slots like Highway King,
Boys King Treasure, and Thai Paradise, along with exciting arcade and fishing games like Ocean King and
Monkey King.</p>
<p class="footer-paragraph">At Bet365Png, we pride ourselves on offering a wide range of trusted pokies from
the best game providers in the industry. Our partners, including renowned names like Microgaming, Playtech,
Pragmatic Play, and others, ensure that every player enjoys top-quality games with exciting features, innovative
designs, and fair gameplay. </p>
<p class="footer-paragraph">Whether you're a seasoned gambler or a new player, you'll find a diverse selection
of pokies that meet your preferences and provide an unforgettable gaming experience. With the support of these
trusted game providers, you can rest assured that your time spent at Bet365Png will be both thrilling and
rewarding.</p>
<h3 class="h3-footer-title">Live Casino</h3>
<p class="footer-paragraph">Experience the authentic atmosphere of a real casino with Bet365Png’s live dealer
games. Play blackjack, roulette, baccarat, and more, all streamed in high definition with real dealers. With
interactive features and various betting options, live casino games provide an immersive and engaging way to
enjoy table games from anywhere.</p>
<h4 class="h4-footer-title">Trusted Live Casino Game Providers at Bet365Png</h4>
<h5 class="h5-footer-title">Sexy Baccarat</h5>
<p class="footer-paragraph">Sexy Baccarat is a premium live dealer gaming provider offering a wide selection
of casino games, including baccarat, blackjack, poker, and slots. Known for its interactive and charismatic dealers,
Sexy Baccarat brings the excitement of real-time, live gaming to players worldwide. The site also offers sports
betting and horse racing, providing a complete gambling experience. Available in English and Chinese, it
welcomes players from across the globe.</p>
<p class="footer-paragraph">Players can enjoy real money gaming with various deposit methods after registering.
New players receive a welcome bonus, allowing them to explore games without risking their own funds.</p>
<p class="footer-paragraph">Sexy Baccarat offers different game variations, such as Blackjack, Baccarat,
Roulette, Craps, and Poker, each with unique rules and betting options. Enjoy fast payouts with instant deposits,
ensuring a seamless gaming experience. Additionally, players can compete in tournaments and win cash prizes,
with the chance to hit some of the largest jackpots in the industry.</p>
<h5 class="h5-footer-title">Big Gaming (BG Gaming)</h5>
<p class="footer-paragraph">Big Gaming, also known as BG Gaming, is a prominent gaming product provider in
Asia, recognised for its reliable and high-quality systems. Specialising in secure and innovative gaming solutions,
BG Gaming offers a range of API products including BG Live Casino, BG Poker, BG Galaxy Treasure, BG Daisen
Fishing, BG XiYou Fishing, and BG Fishing Master.</p>
<p class="footer-paragraph">Players can enjoy real money gaming with various deposit methods after registering.
New players receive a welcome bonus, allowing them to explore games without risking their own funds.</p>
<p class="footer-paragraph">With a skilled technical team and cutting-edge systems, BG Gaming delivers
industry-leading products that help operators seize market opportunities and maximize success. Known for its
stability and excellence, BG Gaming provides secure, high-performance products designed to meet the demands
of the iGaming market.</p>
<h3 class="h3-footer-title">Sports Betting</h3>
<p class="footer-paragraph">Bet on local and international sports with Bet365Png’s competitive odds and wide
betting markets. Whether it’s football, rugby, basketball, or esports, players can wager on pre-match and live
events, take advantage of high odds, and enjoy fast payouts. With in-play betting and real-time updates, sports
fans can stay engaged throughout the game.</p>
<h4 class="h4-footer-title">Trusted Sports Betting Game Providers at Bet365Png</h4>
<h5 class="h5-footer-title ">SV388</h5>
<p class="footer-paragraph">SV388 is a premier sports betting provider specialising in live cockfighting events,
delivering an immersive and authentic experience for bettors worldwide. With a well-structured event calendar,
SV388 ensures non-stop action, featuring matches across different time zones. Each event is meticulously
organised, with fights categorised by arenas and match numbers, ensuring transparency and fair play.</p>
<p class="footer-paragraph">Players can engage in real-time betting on scheduled matches, analysing fight
histories and odds to make informed wagers. SV388’s advanced platform offers seamless streaming, secure
transactions, and a user-friendly interface, making it the top choice for cockfight betting enthusiasts.</p>
<h5 class="h5-footer-title">RCB988</h5>
<p class="footer-paragraph">RCB988 is a premier horse racing betting platform offering a seamless wagering
experience on top racing events across Asia and beyond. Designed for both desktop and mobile users, RCB988
provides an extensive selection of betting markets, real-time race updates, and fast withdrawals, ensuring a
smooth and rewarding betting experience.</p>
<p class="footer-paragraph">With coverage of major horse racing, dog racing, and harness racing events,
players can place win, place, and show bets with competitive odds. Exclusive promotions, including welcome
bonuses and free credit offers, further enhance the betting experience. RCB988's commitment to user
satisfaction extends to its dedicated 24/7 support via live chat, Telegram, and WhatsApp, making it a trusted
choice for horse racing enthusiasts.</p>
<h5 class="h5-footer-title">E1 Sport</h5>
<p class="footer-paragraph">E1 Sport is a premier esports betting provider, offering a dynamic platform for fans
to engage with top-tier competitive gaming. With an extensive selection of betting markets covering major titles
like Dota 2, CS:GO, and League of Legends, E1 Sport brings the excitement of esports to bettors worldwide.</p>
<p class="footer-paragraph">Holding a valid Malta Gaming Authority license, E1 Sport ensures a secure and
reliable betting environment. Its user-friendly platform, available on both desktop and mobile apps for iOS and
Android, allows seamless access to esports wagers. With diverse bet types such as match-winner, handicap,
and correct score, E1 Sport delivers an immersive and thrilling betting experience. Responsive support via live
chat, WhatsApp, and Telegram ensures a smooth and hassle-free user experience.</p>
<p class="footer-paragraph">Bet365Png offers a diverse selection of trusted sports betting game providers,
ensuring an unparalleled betting experience for players. With a commitment to quality, security, and innovation,
these providers bring a vast range of sports and esports betting options, competitive odds, and seamless
platforms for both desktop and mobile users. </p>
<p class="footer-paragraph">Whether you're into traditional sports, horse racing, or esports, Bet365Png delivers
top-tier gaming backed by reputable providers. Explore the excitement, place your bets with confidence, and
enjoy the best that the sports betting industry has to offer.</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title">Casino Online Bonuses in Papua New Guinea at Bet365Png</h2>
<p class="footer-paragraph">Bet365Png offers a variety of rewarding promotions, giving players extra value
from the moment they sign up. Whether it's free credits, daily deposit boosts, or VIP perks, there's something
for everyone.</p>
<h3 class="h3-footer-title">Unlock Free Credits</h3>
<p class="footer-paragraph">Start playing with free rewards through simple activities:</p>
<ul class="disc-list">
<li><b>Sign-Up Bonus: </b>Get PGK20 just for registering.</li>
<li><b>Golden Egg Challenge: </b>Deposit $1 to earn 1 point.</li>
<li><b>Daily App Bonus: </b>Download the app and claim free PGK daily.</li>
<li><b>Social Rewards: </b>Earn PGK7 for sharing on Facebook or refer a friend for PGK10.</li>
</ul>
<h3 class="h3-footer-title">Boost Your Deposits</h3>
<p class="footer-paragraph">Make the most of every deposit with these exclusive offers:</p>
<ul class="disc-list">
<li><b>100% Welcome Bonus </b>with no withdrawal limits.</li>
<li><b>20% First Daily Deposit Bonus </b>to start the day with extra funds.</li>
<li><b>Unlimited 5% & 10% Bonuses </b>on deposits for consistent rewards.</li>
</ul>
<h3 class="h3-footer-title">VIP Perks for Loyal Players</h3>
<p class="footer-paragraph">High rollers and frequent players can enjoy exclusive cash rewards based on their VIP level:</p>
<ul class="disc-list">
<li><b>PGK38 </b> for VIP Level 1</li>
<li><b>PGK98 </b> for VVIP Level 2</li>
<li><b>PGK288 </b> for VVIP Level 3</li>
<li><b>PGK588 </b> for VVIP Level 4</li>
<li><b>PGK888 </b> for VVIP Level 5</li>
</ul>
<p class="footer-paragraph">With these exciting bonuses, Bet365Png ensures every player gets more chances to win while enjoying their favorite casino games.</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title">VIP Benefits at Bet365Png Online Casino</h2>
<p class="footer-paragraph">Bet365Png rewards its most dedicated players with an exclusive VIP program
packed with special perks and free credit bonuses. As players move up the ranks, they unlock bigger rewards
and more exciting benefits.</p>
<h3 class="h3-footer-title">VIP Ranks & How to Qualify</h3>
<p class="footer-paragraph">Players can progress through five VIP levels by meeting the required deposit
thresholds. Each level comes with increasing rewards, giving loyal members extra incentives to keep playing.</p>
<ul class="disc-list">
<li><b>Loyalty VIP 1: </b>Deposit PGK500+ to qualify.</li>
<li><b>Loyalty VVIP 2: </b>Deposit PGK2,000+ to qualify.</li>
<li><b>Loyalty VVIP 3: </b>Deposit PGK5,000+ to qualify.</li>
<li><b>Loyalty VVIP 4: </b>Deposit PGK10,000+ to qualify.</li>
<li><b>Loyalty VVIP 5: </b>Deposit PGK20,000+ to qualify.</li>
</ul>
<h3 class="h3-footer-title">VIP Bonus Rewards</h3>
<p class="footer-paragraph">Each VIP level comes with a free credit bonus that can be used on slot games.
Players must meet a simple 1x wagering requirement before making a withdrawal.</p>
<ul class="disc-list">
<li><b>VIP 1: </b>Free PGK38</li>
<li><b>VVIP 2: </b>Free PGK98</li>
<li><b>VVIP 3: </b>Free PGK288</li>
<li><b>VVIP 4: </b>Free PGK588</li>
<li><b>VVIP 5: </b>Free PGK888</li>
</ul>
<p class="footer-paragraph">All VIP rewards require a minimum withdrawal of PGK50 and can only be used on slot games.</p>
<p class="footer-paragraph">Bet365Png’s VIP program ensures that loyal players get the recognition they
deserve, with increasing benefits as they level up. Start playing today and climb the ranks to claim bigger
rewards!</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title">Payment Methods at Online Casino Bet365Png</h2>
<p class="footer-paragraph">Bet365Png provides secure and convenient payment options for players, ensuring
smooth transactions for both deposits and withdrawals. The platform supports two of the most trusted banking
institutions in Papua New Guinea: BSP and Kina Bank.</p>
<h3 class="h3-footer-title">BSP (Bank South Pacific)</h3>
<p class="footer-paragraph">BSP is the largest bank in Papua New Guinea, offering reliable and efficient
banking services. Players can use BSP for fast deposits and withdrawals, ensuring a hassle-free gaming
experience.</p>
<ul class="disc-list">
<li>Instant deposits for quick access to funds.</li>
<li>Secure withdrawals processed within a reasonable timeframe.</li>
<li>Available via online banking, mobile banking, and bank transfers.</li>
</ul>
<h3 class="h3-footer-title">Kina Bank</h3>
<p class="footer-paragraph">Kina Bank is another popular choice among Bet365Png players, known for its easy
and secure online transactions.</p>
<ul class="disc-list">
<li>Fast and seamless deposits to fund gaming accounts.</li>
<li>Efficient withdrawal processing for cashing out winnings.</li>
<li>Compatible with online banking and mobile transactions.</li>
</ul>
<p class="footer-paragraph">With BSP and Kina Bank, Bet365Png ensures that players have secure,
convenient, and reliable payment options to enjoy a smooth gaming experience.</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title">Ensuring Responsible Gaming at Bet365Png</h2>
<p class="footer-paragraph">At Bet365Png, responsible gaming is a top priority. The platform is committed to
providing a safe and enjoyable betting experience while ensuring that players maintain control over their
gambling habits. By implementing key measures and offering support resources, Bet365Png encourages
responsible gaming across Papua New Guinea.</p>
<h3 class="h3-footer-title">Setting Limits for Safer Play</h3>
<p class="footer-paragraph">To help players manage their gambling activities, Bet365Png provides various
self-regulation tools, including:</p>
<ul class="disc-list">
<li><b>Deposit Limits: </b>Players can set daily, weekly, or monthly deposit caps to control spending.</li>
<li><b>Wagering Limits: </b>Adjustable limits prevent excessive betting beyond one’s budget.</li>
</ul>
<h3 class="h3-footer-title">Recognising Problem Gambling</h3>
<p class="footer-paragraph">It’s essential for players to recognise the signs of problem gambling, such as:</p>
<ul class="disc-list">
<li>Chasing losses or betting beyond affordable limits.</li>
<li>Gambling affecting personal or financial well-being.</li>
<li>Difficulty stopping or controlling gaming habits.</li>
</ul>
<h3 class="h3-footer-title">Support and Assistance</h3>
<p class="footer-paragraph">Bet365Png promotes responsible gaming by directing players to professional
support organizations that offer guidance and assistance. Players who need help can reach out to local and
international responsible gambling services for support.</p>
<p class="footer-paragraph">By fostering a safe and regulated gaming environment, Bet365Png ensures that
players in Papua New Guinea can enjoy betting responsibly while minimising risks associated with gambling.</p>
</div>
<div class="footer-box-sub">
<h2 class="h2-footer-title">FAQs</h2>
<h3 class="h3-footer-title">What is the best online casino game to win money?</h3>
<p class="footer-paragraph">Games with a low house edge, such as blackjack and baccarat, offer better winning chances. Progressive jackpot slots can also provide big payouts.</p>
<h3 class="h3-footer-title">What online casino game is easiest to win?</h3>
<p class="footer-paragraph">Blackjack and baccarat have the highest odds of winning due to their simple strategies and low house edge.</p>
<h3 class="h3-footer-title">How to play online casinos in Papua New Guinea?</h3>
<p class="footer-paragraph">Register an account at a trusted online casino like Bet365Png, deposit funds, choose your preferred game, and place your bets. Always check the game rules before playing.</p>
<h3 class="h3-footer-title">What online casino games can you win real money?</h3>
<p class="footer-paragraph">You can win real money on pokies, table games, live dealer games, and sports betting, provided you play with real money and meet wagering requirements.</p>
<h3 class="h3-footer-title">How to play casino slots online?</h3>
<p class="footer-paragraph">Select a pokies game, set your bet size, and spin the reels. Winning depends on symbol combinations and bonus features.</p>
</div>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Footer Section</title>
<!-- 引入外部 CSS -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<footer>
</div>
<footer>
<!-- License & Certificate -->
<div class="section-title">License & Certificate</div>
<div class="icons-row">
<img src="https://static.gwvkyk.com/media/11684a510fa86a2dd5361.png">
<img src="https://static.gwvkyk.com/media/8f2b6d7cfea8647a2d1ef.png">
<img src="https://static.gwvkyk.com/media/93a5359cfea86d6fbc084.png">
<img src="https://static.gwvkyk.com/media/d261d4bcfea86c7edc758.png">
<img src="https://static.gwvkyk.com/media/09d354dcfea86862150dc.png">
<img src="https://static.gwvkyk.com/media/fb0a21fcfea86e9538c4e.png">
<img src="https://static.gwvkyk.com/media/6e75310dfea8664d53427.png">
<img src="https://static.gwvkyk.com/media/e9473f0dfea86d6b851a3.png">
</div>
<!-- Responsible Gambling -->
<div class="section-title">Responsible Gambling</div>
<div class="icons-row">
<img src="https://static.gwvkyk.com/media/1d27100cfea86b5aff6c3.png">
<img src="https://static.gwvkyk.com/media/9692972cfea86d1dced4e.png">
<img src="https://static.gwvkyk.com/media/70932b3cfea8606cef8fc.png">
<img src="https://static.gwvkyk.com/media/6a10dc4cfea861b10d362.png">
</div>
<hr>
<!-- Bottom -->
<div class="footer-bottom">
Need Help? Talk To Live Chat <br>
© 2025 BET365PNG
</div>
</footer>
</body>
</html>
<!----reviews---->
<script>
(async function insertReviewFormAndSlider() {
const allowedUserID = "xxxxxx"; // only this user can see form
const userID = "@UserID"; // SPA will replace dynamically
// --- Wait for element to exist (SPA-safe) ---
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const interval = 100;
let elapsed = 0;
const timer = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(timer);
resolve(el);
}
elapsed += interval;
if (elapsed >= timeout) clearInterval(timer) || reject(null);
}, interval);
});
}
// Wait for livetx-wrapper to exist
const livetxWrapper = await waitForElement(".livetx-wrapper").catch(() => null);
if (!livetxWrapper) return;
// --- Create Review Form ---
const formContainer = document.createElement("div");
formContainer.id = "review-form-container";
formContainer.innerHTML = `
<form id="review-form" onsubmit="return false;">
<input type="hidden" name="username" id="review-username" value="${userID}" />
<input type="text" name="title" placeholder="Review title" required />
<textarea name="content" placeholder="Write your review..." required></textarea>
<div class="rating-container">
<span class="rating-label">Rating:</span>
<div class="rate-stars" id="rate-stars">
<span data-value="1">★</span>
<span data-value="2">★</span>
<span data-value="3">★</span>
<span data-value="4">★</span>
<span data-value="5">★</span>
</div>
<input type="hidden" name="rating" id="rating-value" value="5" />
</div>
<button type="button" id="review-submit-btn">Submit Review</button>
<div id="review-form-message" style="margin-top:5px;"></div>
</form>
`;
// --- Create Reviews Slider ---
const sliderContainer = document.createElement("div");
sliderContainer.className = "swiper reviews-slider";
sliderContainer.innerHTML = `<div class="swiper-wrapper" id="reviews-wrapper"></div>`;
// Insert slider and form **after** livetx-wrapper
livetxWrapper.parentNode.insertBefore(sliderContainer, livetxWrapper.nextSibling);
livetxWrapper.parentNode.insertBefore(formContainer, sliderContainer.nextSibling);
// --- Add CSS ---
const style = document.createElement("style");
style.textContent = `
/* Form */
#review-form-container {
max-width: 400px;
margin: 20px auto;
background: url('https://static.gwvkyk.com/media/d39c5a0368476af89caf7.jpg');
background-size: 100% 100%;
padding: 20px 30px;
border-radius: 8px;
color: #FFFFFF;
border: 2px solid #00EBBA;
display: none;
}
#review-form input, #review-form textarea {
width: 100%; margin-bottom: 10px; padding: 8px;
border-radius: 8px;
border: 2px solid #00EBBA;
background: #000000;
color: #FFFFFF;
}
#review-form button {
background: linear-gradient(to bottom, #1f410b 70%, #05E1B1 100%);
border: 2px solid #00EBBA;
padding: 10px 15px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
color: #FFFFFF;
width: 100%;
}
#review-form button:hover { opacity: 0.9; }
.rating-container {
padding: 8px; margin-bottom: 10px;
border: 2px solid #00EBBA;
background: #000000;
border-radius: 15px;
text-align: left;
}
.rate-stars { display: flex; gap: 5px; font-size: 24px; cursor: pointer; }
.rate-stars span { color: #ffffff4d; transition: color 0.15s; }
.rate-stars span.active { color: #ffea00; }
/* Slider */
.reviews-slider { width: 100%; overflow: hidden; cursor: grab; max-width: 600px; margin: 20px auto; }
.swiper-slide.review-slide { flex-shrink: 0; }
.review-card {
position: relative; display: flex; flex-direction: column;
justify-content: space-between; border-radius: 8px;
padding: 10px 30px;
color: #FFFFFF;
min-height: 180px;
max-height: 180px;
box-sizing: border-box;
overflow: hidden;
}
.review-card .card-bg {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
object-fit: fill; z-index: 0; border-radius: 8px;
}
.review-card > *:not(.card-bg) { position: relative; z-index: 1; }
.review-header { font-weight: bold; margin-bottom: 5px;color: #ffea00; }
.review-content { font-size: 14px; margin-bottom: 8px; flex-grow: 1; }
.review-footer { margin-top: auto; }
.stars { color: #ffea00; margin-bottom: 4px; }
.review-user { font-size: 12px; color: #FFFFFF; }
/*@media (max-width: 768px) {
.review-card {
min-height: 150px;
}
}*/
`;
document.head.appendChild(style);
// --- Form Init ---
function initReviewForm() {
const usernameInput = document.getElementById("review-username");
//if (usernameInput.value.trim() !== allowedUserID) return;
document.getElementById("review-form-container").style.display = "block";
const form = document.getElementById("review-form");
const submitBtn = document.getElementById("review-submit-btn");
const messageDiv = document.getElementById("review-form-message");
const stars = document.querySelectorAll("#rate-stars span");
const ratingInput = document.getElementById("rating-value");
const FORM_API = "https://reviewssys-1.minigame9.work/api/api.php?type=submit&platform_id=5";
function highlightStars(rating) {
stars.forEach(s => s.classList.toggle("active", +s.dataset.value <= +rating));
}
highlightStars(+ratingInput.value);
stars.forEach(s => s.addEventListener("click", () => {
const val = +s.dataset.value;
ratingInput.value = val;
highlightStars(val);
}));
submitBtn.addEventListener("click", async () => {
messageDiv.textContent = "Submitting...";
messageDiv.style.color = "#fff";
submitBtn.disabled = true;
const formData = new FormData(form);
formData.append("platform", "1");
try {
const res = await fetch(FORM_API, { method: "POST", body: formData });
const data = await res.json();
if (data.success) {
messageDiv.style.color = "limegreen";
messageDiv.textContent = data.message || "Review submitted successfully!";
form.reset();
ratingInput.value = 5;
highlightStars(5);
} else {
messageDiv.style.color = "red";
messageDiv.textContent = data.error || "Failed to submit review.";
// ✅ Check for "Username is required" and redirect
if (data.error && data.error.toLowerCase().includes("username is required")) {
setTimeout(() => {
window.location.href = "/login";
}, 1000); // redirect after 1.5 seconds
}
}
} catch {
messageDiv.style.color = "red";
messageDiv.textContent = "Network error, please try again.";
} finally {
submitBtn.disabled = false;
}
});
}
// --- Helper: render reviews into the slider ---
function renderReviews(reviews) {
const wrapper = document.getElementById("reviews-wrapper");
if (!wrapper) return;
wrapper.innerHTML = "";
reviews.forEach(review => {
const slide = document.createElement("div");
slide.className = "swiper-slide review-slide";
slide.innerHTML = `
<div class="review-card">
<img src="/media/d39c5a0368476af89caf7.jpg" class="card-bg" />
<div class="review-header">${review.title}</div>
<div class="review-content">${review.content}</div>
<div class="review-footer">
<div class="stars">${"★".repeat(review.rating)}${"☆".repeat(5 - review.rating)}</div>
${review.mobile ? `<div class="review-user">${review.mobile}</div>` : ""}
</div>
</div>`;
wrapper.appendChild(slide);
});
// --- Duplicate slides for smooth infinite scroll ---
const slides = wrapper.querySelectorAll(".swiper-slide");
slides.forEach(s => wrapper.appendChild(s.cloneNode(true)));
// --- Load Swiper only once ---
if (!window.Swiper) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css";
document.head.appendChild(link);
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js";
script.onload = initSwiper;
document.head.appendChild(script);
} else {
initSwiper();
}
}
// --- Helper: initialize Swiper autoplay slider ---
function initSwiper() {
new Swiper(".reviews-slider", {
slidesPerView: 1.4,
spaceBetween: 10,
loop: true,
freeMode: true,
freeModeMomentum: false,
allowTouchMove: true,
speed: 10000,
autoplay: {
delay: 0,
disableOnInteraction: false,
pauseOnMouseEnter: false,
},
});
}
// --- Slider Init ---
async function initReviewSlider() {
//if (userID.trim() !== allowedUserID) return;
const API_URL = "https://reviewssys-1.minigame9.work/api/api.php?type=list&platform_id=5";
const now = Date.now();
const lastFetch = localStorage.getItem("lastReviewFetch");
const cachedReviews = localStorage.getItem("cachedReviews");
// ✅ Use cached reviews if last fetch < 1 minute ago
if (lastFetch && now - lastFetch < 60 * 1000 && cachedReviews) {
renderReviews(JSON.parse(cachedReviews));
return;
}
// 🛰️ Fetch new reviews from API
try {
const res = await fetch(API_URL);
const data = await res.json();
if (!data.success || !data.reviews) return;
// ✅ Save to localStorage
localStorage.setItem("lastReviewFetch", Date.now());
localStorage.setItem("cachedReviews", JSON.stringify(data.reviews));
renderReviews(data.reviews);
} catch (err) {
// 🔄 Fallback: use cached data if available
if (cachedReviews) renderReviews(JSON.parse(cachedReviews));
}
}
//initReviewForm();
initReviewSlider();
})();
</script>
# Reglas de Construcción de Proyectos Laravel
> Guía general de cómo construir un proyecto Laravel de forma **eficiente, escalable, mantenible y reutilizable**.
> Este documento es un conjunto de reglas base; aplicarlas garantiza consistencia en todo el proyecto.
> Todo cambio al proyecto debe quedar registrado (ver `ultimosCambios.md` si existe en el proyecto).
---
## Índice
1. [Arquitectura MVC](#1-arquitectura-mvc)
2. [Estructura de carpetas](#2-estructura-de-carpetas)
3. [Migraciones: orden I, P, R, A, T + ÍNDICES](#3-migraciones-orden-i-p-r-a-t--índices)
4. [Seeders](#4-seeders)
5. [Modelos (M) y Eloquent](#5-modelos-m-y-eloquent)
6. [Controladores (C) — Controladores delgados](#6-controladores-c--controladores-delgados)
7. [Vistas (V) — HTML semántico, Bootstrap y CSS](#7-vistas-v--html-semántico-bootstrap-y-css)
8. [Paleta de colores](#8-paleta-de-colores)
9. [Principios SOLID](#9-principios-solid)
10. [TypeScript](#10-typescript)
11. [Rendimiento: lo que SÍ vuelve lenta la página](#11-rendimiento-lo-que-sí-vuelve-lenta-la-página)
12. [Rendimiento: lo que NO afecta (gratis para la GPU)](#12-rendimiento-lo-que-no-afecta-gratis-para-la-gpu)
13. [Seguridad](#13-seguridad)
14. [Repositorio/Control de versiones](#14-repositoriocontrol-de-versiones)
15. [Iniciar el proyecto con acceso global (cloudflared)](#15-iniciar-el-proyecto-con-acceso-global-cloudflared)
16. [Validación de campos: reglas por campo + jQuery Validate](#16-validación-de-campos-reglas-por-campo--jquery-validate)
17. [CSS Flexbox — Reglas de uso](#17-css-flexbox--reglas-de-uso)
18. [Navegación: botones Cancelar y Volver](#18-navegación-botones-cancelar-y-volver)
19. [Auditoría de acciones de usuarios](#19-auditoría-de-acciones-de-usuarios)
---
## 1. Arquitectura MVC
Laravel usa el patrón **Modelo–Vista–Controlador (MVC)**. Cada pieza tiene UNA responsabilidad clara:
- **Modelo (M):** representa una tabla de la base de datos y su lógica de negocio. En `app/Models/`.
- **Vista (V):** solo presentación (HTML/CSS). Sin lógica de negocio. En `resources/views/`.
- **Controlador (C):** el "orquestador": recibe la petición, delega al modelo/servicio y devuelve una vista o JSON. Debe ser **delgado**.
Regla de oro: **vista = presentación, modelo = datos, controlador = coordinación**.
---
## 2. Estructura de carpetas
```
app/
├── Http/
│ ├── Controllers/ # Controladores (delgados, uno por recurso)
│ │ └── Admin/ # Controladores de panel admin/backoffice
│ ├── Requests/ # Request personalizados (validación)
│ └── Middleware/ # Middleware (auth, roles, país, etc.)
├── Models/ # Modelos Eloquent (uno por tabla de negocio)
├── Services/ # Lógica de negocio reutilizable (fuera de controladores)
├── Providers/ # Providers (registro de servicios, etc.)
└── helpers.php # Funciones helper globales puras
```
- **Un archivo por clase.**
- Los **Servicios** (`app/Services/`) albergan la lógica compleja y reutilizable: envío de archivos, tasas de cambio, backups, reportes, etc. Los controladores **llaman** a los servicios, no implementan esa lógica.
- Los **Requests** (`app/Http/Requests/`) contienen la validación de entrada; un controlador nunca llena el método `store()` de `if (...) validate(...)`.
- Las **rutas** se organizan por dominio en `routes/` (web, auth, api, console).
---
## 3. Migraciones: orden I, P, R, A, T + ÍNDICES
**Regla obligatoria:** las columnas de cada tabla DEBEN ir en este orden:
| Letra | Sección | Qué contiene | Ejemplo |
|-------|---------|--------------|---------|
| **I** | ID (siempre primero) | `$table->id();` | `$table->id();` |
| **P** | Personal / Datos de negocio | Campos propios de la entidad | `nombre`, `precio`, `estado`, `margen`… |
| **R** | Relaciones | FKs **desacopladas de los modelos, usando strings** | `$table->foreignId('user_id')->constrained('users')->onDelete('cascade');` |
| **A** | Auth (marcar "No aplica" si no hay) | Campos de autenticación (si los hay) | `password`, `remember_token` |
| **T** | Timestamps / Fechas | Fechas personalizadas + `timestamps()` + `softDeletes()` | `fecha_emision`, `$table->timestamps()`, `$table->softDeletes()` |
> **Nota:** Los estados (boolean) los metemos dentro de 'P' o justo antes de 'T' según prefieras.
Ejemplo modelo ([ver `create_facturas_table.php` del proyecto]):
```php
Schema::create('facturas', function (Blueprint $table) {
// I - ID (siempre primero)
$table->id();
// P - Personal / Datos de negocio
$table->string('serie');
$table->integer('numero');
$table->decimal('total', 10, 2);
// ... más campos de negocio
// Nota: Los estados (boolean) los metemos dentro de 'P'
// o justo antes de 'T' según prefieras.
$table->boolean('estado')->default(true);
// R - Relaciones (desacoplado de modelos, usando strings)
$table->foreignId('cliente_id')->constrained('clientes')->onDelete('cascade');
$table->foreignId('trabajo_id')->nullable()->constrained('trabajos')->onDelete('set null');
// A - Auth (No aplica en esta tabla)
// T - Timestamps / Fechas
$table->date('fecha_emision');
$table->timestamps();
$table->softDeletes();
});
// ÍNDICES para búsquedas (clave del rendimiento)
```
### Índices (obligatorio para optimizar consultas)
Importar `DB` para sentencias SQL puras de Postgres:
```php
use Illuminate\Support\Facades\DB;
```
Agregar índices **por cada consulta frecuente** (CRUD + búsquedas). Tipos recomendados:
- **B-Tree** (default): para igualdad y rangos en columnas consultadas mucho.
`DB::statement('CREATE INDEX idx_facturas_estado ON facturas (estado)');`
- **Compuesto**: para consultas `WHERE col1 = ? AND col2 = ?`.
`DB::statement('CREATE INDEX idx_facturas_cliente_estado ON facturas (cliente_id, estado)');`
- **UNIQUE**: para integridad + velocidad en claves naturales.
`DB::statement('CREATE UNIQUE INDEX idx_facturas_serie_numero ON facturas (serie, numero)');`
- **Índice de expresión**: búsquedas por email sin importar mayúsculas.
`DB::statement('CREATE INDEX idx_clientes_lower_email ON clientes (LOWER(email))');`
- **Índice parcial**: índice diminuto cuando el 80% de tus consultas filtra por una condición.
`DB::statement("CREATE INDEX idx_trabajos_estado ON trabajos (estado) WHERE estado IN ('presupuesto','en_proceso')");`
- **BRIN**: solo en columnas **ordenadas cronológicamente y de gran tamaño** (>1M filas) — created_at, failed_at. Ocupa ~100KB frente a ~50MB de un B-Tree.
- **GIN**: búsqueda de texto completo (`to_tsvector`) en campos largos.
> **Importante (Postgres):** NO usar `NOW()` ni funciones `VOLATILE` dentro de un índice parcial (Postgres lo rechaza). Para limpiar tokens expirados usa un B-Tree normal en `created_at`.
En el `down()` eliminar los índices personalizados antes de soltar la tabla:
```php
DB::statement('DROP INDEX IF EXISTS idx_facturas_estado');
// ...
Schema::dropIfExists('facturas');
```
### NO crear migraciones "de añadido" sueltas
> **Regla obligatoria:** **NUNCA** generar archivos de migración del tipo
> `2026_08_28_211239_add_original_copia_to_facturas_table.php` (ni `add_X_to_..._table`,
> `create_..._table` para tablas que ya existen, etc.). Los campos nuevos/alteraciones
> de una tabla **se agregan en la migración que crea esa tabla** (`create_facturas_table.php`)
> y no en archivos apartes.
>
> - Cuando crees una tabla o añadas columnas, edita SIEMPRE la migración original de esa tabla.
> - Si ya se generó una migración aparte por error, su contenido debe **fusionarse**
> dentro de la migración original de la tabla y la migración suelta debe **eliminarse**
> (rehaciendo con `migrate:fresh --seed` en desarrollo, o con una corrección manual/backup en producción).
> - Mantener un artefacto de migración por tabla mantiene el esquema centralizado y legible.
---
## 4. Seeders
- Un **seeder por tabla** (`UsersSeeder`, `RolesSeeder`, `FacturasSeeder`, …) y un `DatabaseSeeder` que los orquesta en orden de dependencia.
- **Orden correcto:** primero las tablas "madre" (users, roles, clientes, proveedores, materiales) y luego las que dependen (trabajos, facturas, pagos, fotos).
- Los seeders **deben ser idempotentes** y enfocados a **datos de demostración** que permitan probar la app.
- Para `migrate:fresh --seed` funcionar, los seeders respetan los índices UNIQUE (no duplican claves).
- Usar `delete()`/truncate al inicio cuando aplique para evitar duplicados en ejecuciones repetidas.
---
## 5. Modelos (M) y Eloquent
- Nombre en singular y **StudlyCase** (Tabla `facturas` → Modelo `Factura`).
- Declarar `$fillable` (nunca `$guarded = []` a lo loco) y los `$casts` de tipos (`decimal`, `boolean`, `json`, `date`).
- Definir **relaciones** (`belongsTo`, `hasMany`, `belongsToMany`) y usarlas; nunca armar joins a mano en el controlador.
- **No escribir consultas SQL crudas en controladores/vistas**; encapsular la lógica compleja en **Servicios**.
- Aplicar `$hidden` para campos sensibles (password, tokens) al serializar.
- Usar **soft deletes** (`softDeletes()`) para tablas de datos de negocio que admiten papelera; declarar `deleted_at` en el modelo.
- Reglas de validación de creación/edición viven en el **Request**, no en el modelo.
---
## 6. Controladores (C) — Controladores delgados
- **Un controlador por recurso** con las acciones REST (`index`, `create`, `store`, `show`, `edit`, `update`, `destroy`).
- **Nunca debe contener lógica de negocio compleja**; se delega a `app/Services/`.
- **Nunca debe contener validación inline**; se delega al `Request` (form request) correspondiente.
- Respuesta coherente: para peticiones AJAX devuelve JSON, para el resto devuelve `redirect()`/vista.
- Métodos de acceso a datos repetitivos y consultas frecuentes se pueden encapsular (scope en modelo o servicio).
- Coherencia de nombres de rutas, controladores y vistas (`facturas.create` → `FacturaController@create` → `views/facturas/create.blade.php`).
---
## 7. Vistas (V) — HTML semántico, Bootstrap y CSS
### HTML semántico (obligatorio)
- Usar etiquetas semánticas: `<header>`, `<nav>`, `<main>`, `<section>`, `<article>`, `<aside>`, `<footer>` — no `divs` a granel.
- Una sola etiqueta `<main>` por página.
- Encabezados en orden jerárquico (`h1` → `h2` → `h3`), un solo `h1` por página.
- `label` siempre asociado a su `input` (accesibilidad), `alt` en imágenes, `aria-*` en componentes interactivos.
- Usar tablas reales `<table>`, `<thead>`, `<tbody>` para datos tabulares.
### CSS / Layout
- **Bootstrap 5** como base de componentes y grid; las vistas usan el grid de Bootstrap (`container`, `row`, col-*).
- **CSS Box** (caja) y **Media Queries** para responsividad. Modelo de caja: `content-box`/`border-box` definidos de forma global; todo elemento es una caja (margin, border, padding, content).
- Media queries para breakpoints: móvil primero (`min-width`): `576px`, `768px`, `992px`, `1200px`.
- Preferir **CSS Grid / Flexbox** antes que hacks con `float`, `position: absolute` o `margin` negativo.
- Estilos compartidos en el layout base (`layouts/app.blade.php`) como variables CSS (`:root { --c-primary: ...; }`) y clases utilitarias reutilizadas en todo el proyecto.
- **Animaciones suaves (opacity/transform), cero layouts animados** (ver sección de rendimiento).
---
## 8. Paleta de colores
Paleta oficial del proyecto (definida como variables CSS en el layout base):
| Variable | Valor | Uso |
|----------|-------|-----|
| `--c-primary` | `#2563EB` | Color primario (acciones, enlaces, activos) |
| `--c-primary-dark` | `#1D4ED8` | Hover / degradado oscuro |
| `--c-primary-light` | `#3B82F6` | Tints / focus |
| `--bs-navbar-bg` | `linear-gradient(135deg,#1E40AF,#2563EB)` | Barra superior |
| `--bs-success` | `#10B981` | Éxito / pagos completados |
| `--bs-danger` | `#EF4444` | Errores / destrucción |
| `--bs-warning` | `#F59E0B` | Advertencias / pendiente |
| `--bs-info` | `#06B6D4` | Información |
| `--bs-body-bg` | `#F8FAFD` | Fondo de la aplicación |
| `--bs-body-color` | `#1E293B` | Texto principal |
| `--bs-border-color` | `#E2E8F0` | Bordes |
| `--bs-font-sans-serif` | `'DM Sans', system-ui` | Tipografía principal |
Reglas de color:
- **Primario = azul `#2563EB`**. Gradiente de botones: `linear-gradient(135deg, var(--c-primary), var(--c-primary-dark))`.
- Usar siempre **variables CSS**, nunca colores quemados en cada vista.
- Contraste accesible: texto sobre primario = blanco `#fff`.
---
## 9. Principios SOLID
- **S – Responsabilidad única:** cada clase hace una sola cosa. Un controlador no calcula tasas de cambio; eso va en un `Service`.
- **O – Abierto/cerrado:** extender comportamiento sin modificar el código existente. Ej.: nuevo método de tasa en un Service sin tocar el controlador.
- **L – Sustitución de Liskov:** clases hijas sustituyen a la padre sin romper el contrato.
- **I – Segregación de interfaces:** interfaces pequeñas y específicas.
- **D – Inversión de dependencias:** depender de abstracciones, no de implementaciones concretas. Usar el **contenedor de servicios de Laravel** (bind singleton en `AppServiceProvider`, inyección por constructor) en lugar de instanciar dependencias a mano.
Ejemplo aplicado en el proyecto: `TasaCambioService` se registra como singleton en `AppServiceProvider` y se inyecta donde se necesita.
---
## 10. TypeScript
- Los scripts de cliente serios y de lógica compleja se escriben en **TypeScript** (tipado estático, más mantenible y menos propenso a errores), compilado a JS.
- Se compilan con bundler (Vite por defecto en Laravel 11+).
- El **JavaScript vanilla** (jQuery/JS plano en Blade) se reserva solo para mejoras de UX pequeñas e interactividad ligera dentro de las vistas (validaciones, modales, AJAX de formularios).
- Reglas: evitar `any`, definir tipos/interfaces para los datos (p. ej. respuestas AJAX), funciones puras y pequeñas, sin lógica de negocio en el front (eso va en el backend).
- No bloquear el hilo principal: las peticiones de red son asíncronas (fetch/AJAX).
---
## 11. Rendimiento: lo que SÍ vuelve lenta la página (y consume recursos)
- **`backdrop-filter: blur()` (efecto Glassmorphism):** es de las propiedades más pesadas de CSS. Obliga a la GPU a desenfocar **en tiempo real** lo que hay detrás mientras el usuario hace scroll. En móviles de gama media/baja, abusar de `blur()` en varios elementos provoca tirones y sobrecalentamiento. **Evitar o usar con mucha moderación.**
- **Animar propiedades de layout (`width`, `height`, `margin`, `padding`):** si animas el ancho o los márgenes en un `:hover`, el navegador recalcula **todo el Box Model** en cada fotograma (60 veces/segundo) → *Jank* (lag visual). **No animar layout.**
- **Sombras complejas (`box-shadow` difuminados y múltiples):** dibujar sombras gigantes o superpuestas requiere muchos cálculos de pintura (*Paint*). Usar sombras pequeñas y pocas.
- **Consultas N+1 en backend:** evitar `whereHas`/bucles que lanzan una query por fila; usar `with()` (eager loading).
- **Scripts/imágenes pesados sin optimizar:** comprimir imágenes y retrasar cargas no críticas.
---
## 12. Rendimiento: lo que NO afecta (gratis para la GPU)
- **Transformaciones y opacidad (`transform` y `opacity`):** animar `translateY()`, `scale()` o transparencias **no** recalcula el Box Model ni repinta; ocurre directamente en la GPU (etapa *Composite*) y corre a **60 FPS fluidos** incluso en teléfonos económicos. **Preferir SIEMPRE transform/opacity para animar.**
- **Estructura Bento Grid y CSS Grid / Flexbox:** el motor del navegador está hiperoptimizado para distribuir espacio. Un layout Bento o un grid de tarjetas no añade impacto negativo.
- **Bordes sólidos y colores planos (`border`, `background-color`):** pintar bordes sólidos y fondos planos es barato para la GPU.
- **Columnas indexadas:** que una columna esté indexada acelera la consulta (ver sección de migraciones/índices).
---
## 13. Seguridad
- **Nunca** exponer secretos (APP_KEY, contraseñas, tokens) en código o en repositorios.
- Usar **validación por Form Requests** (nunca confiar en la entrada del usuario).
- Escapar salidas en Blade (`{{ }}`) — Blade lo hace por defecto; no usar `{!! !!}` salvo justificación segura.
- Proteger rutas con middleware de **autenticación** y de **roles/permisos**.
- **CSRF** en todos los formularios (`@csrf`) y **verificación de propiedad** en los recursos (que un usuario solo acceda a lo suyo).
- `APP_DEBUG=false` en producción. Conexión a BD con credenciales en `.env` (nunca en el código).
---
## 14. Repositorio/Control de versiones
- Incluir `.env` en `.gitignore` (junto a `vendor/` y `node_modules/`).
- Commits pequeños y descriptivos, en el idioma del proyecto.
- Registrar cada cambio importante en `ultimosCambios.md` con versión, fecha, descripción y archivos afectados.
- Documentación/estructura versionada junto con el código para reutilizar el proyecto como plantilla.
---
## 15. Iniciar el proyecto con acceso global (cloudflared)
Esta sección explica, paso a paso, cómo poner el proyecto en línea con **acceso global** usando un **túnel cloudflared** (trycloudflare). Sigue este orden siempre; está pensada para que una IA o un desarrollador lo entienda y lo ejecute sin ambigüedad.
### Objetivo
Que la aplicación Laravel (que corre en un servidor local) sea accesible desde **cualquier dispositivo fuera de la red local** mediante una **URL pública** de Cloudflare, sin necesidad de abrir puertos ni tener IP pública.
### Prerrequisitos (antes de empezar)
1. **PostgreSQL** activo y con las credenciales correctas en `.env` (`DB_HOST`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`).
2. Migraciones aplicadas (`php8.4 artisan migrate`).
3. El binario de cloudflared instalado. En este entorno está en: `/home/jdrodriguezg/.local/bin/cloudflared`.
> **CRÍTICO:** usar **`php8.4`** para todos los comandos `php`/`artisan`. El `php` por defecto del sistema es **php7.4** y NO sirve para este proyecto.
### Paso 1 — Levantar el servidor local de Laravel
Ejecutar (si no está ya corriendo):
```bash
nohup php8.4 artisan serve --host=0.0.0.0 --port=8002 > /tmp/opencode/serve.log 2>&1 &
```
Explicación de cada parámetro:
- `nohup ... &` → el proceso sigue corriendo en segundo plano aunque se cierre la terminal.
- `--host=0.0.0.0` → escucha en todas las interfaces de red (obligatorio para que el túnel se pueda conectar).
- `--port=8002` → puerto del servidor (debe coincidir con el del túnel).
- `> /tmp/opencode/serve.log 2>&1` → guarda la salida y los errores en un log.
Verificar que está a la escucha:
```bash
ss -ltnp | grep 8002
# Debe mostrar: LISTEN 0.0.0.0:8002 ...
```
### Paso 2 — Levantar el túnel cloudflared (URL pública)
Ejecutar:
```bash
nohup /home/jdrodriguezg/.local/bin/cloudflared tunnel --url http://localhost:8002 > /tmp/opencode/tunnel.log 2>&1 &
```
Explicación:
- `--url http://localhost:8002` → el túnel redirige el tráfico público al servidor local.
- `> /tmp/opencode/tunnel.log 2>&1` → guarda la salida y, muy importante, **contiene la URL generada**.
### Obtención de la URL pública (IMPORTANTE)
Leer el log del túnel para obtener la URL generada:
```bash
cat /tmp/opencode/tunnel.log
```
Buscar la línea que contiene **"Your quick Tunnel has been created"**; justo debajo aparece la URL:
```
https://XXXX-XXXX.trycloudflare.com
```
Esa es la **URL pública de acceso global** (p. ej. `https://built-financial-choice-explicitly.trycloudflare.com`).
> **⚠️ ADVERTENCIA CLAVE:** la URL de un túnel **quick trycloudflare es ALEATORIA y EFÍMERA**. **Cambia en CADA reinicio** del proceso de cloudflared.
>
> - Si el proceso de cloudflared se cae o se reinicia (corte de luz, reboot, etc.), **hay que volver a leer la URL nueva** del log tal como se explica arriba, porque la anterior **ya no funciona**.
> - Actualizar siempre esa URL en la documentación (p. ej. en `ultimosCambios.md`) para no trabajar con una URL vieja.
### Paso 3 — Verificación
Comprobar que todo responde correctamente:
```bash
# Acceso local (debe devolver 200)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8002/login
# Acceso global a través del túnel (debe devolver 200)
# Sustituir <URL-DEL-TUNEL> por la URL obtenida en el Paso 2
curl -s -o /dev/null -w "%{http_code}\n" https://<URL-DEL-TUNEL>/login
```
Si ambos devuelven `200`, el proyecto está en línea y accesible desde cualquier parte del mundo con la URL pública.
### Nota sobre producción
Los túneles **quick trycloudflare son GRATUITOS pero NO tienen garantía de uptime**; son ideales para pruebas y experimentación. Para producción real se recomienda un **túnel named (con nombre)** y un **dominio propio** configurado en Cloudflare. Un dominio propio cuesta alrededor de 1–15 USD/año.
---
> **Importante:** estas reglas son la base para construir **cualquier proyecto Laravel eficiente** y se pueden reutilizar como plantilla. Mantener consistencia en MVC, migraciones (I-P-R-A-T + índices), seeders, servicios, vistas semánticas, paleta de colores, SOLID, TypeScript, rendimiento y el arranque con cloudflared.
---
## 16. Validación de campos: reglas por campo + jQuery Validate + Máscaras
### 16.1 Dependencias obligatorias (todo proyecto Laravel)
Todo proyecto Laravel **debe incluir** estas dos librerías jQuery en `public/js/` y cargarlas globalmente en `layouts/app.blade.php` después de jQuery:
| Librería | Archivo | Propósito |
|----------|---------|-----------|
| **jQuery Validate** | `public/js/jquery.validate.min.js` | Validación en tiempo real de formularios |
| **jQuery Mask** | `public/js/jquery.mask.min.js` | Máscaras de formato en inputs (teléfono, email, cédula) |
**Orden de carga obligatorio en el layout:**
```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="{{ asset('js/jquery.validate.min.js') }}"></script>
<script src="{{ asset('js/jquery.mask.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
```
> **Regla:** Estas librerías **siempre** se usan. No crear formularios sin validación JS ni máscaras de formato. Copiar los archivos `.min.js` de `public/js/` del proyecto base si no existen.
### 16.2 Uso en una vista
Cada formulario que requiera validación JS debe usar `@push('scripts')` al final del archivo:
```blade
@push('scripts')
<script>
$(function() {
$('#miFormulario').validate({
rules: {
campo_nombre: { required: true, minlength: 2, maxlength: 100 },
campo_email: { required: true, email: true },
},
messages: {
campo_nombre: { required: 'El nombre es obligatorio.', minlength: 'Mínimo 2 caracteres.' },
campo_email: { required: 'El email es obligatorio.', email: 'Ingrese un email válido.' },
},
errorClass: 'is-invalid',
errorElement: 'div',
errorPlacement: function(error, element) {
error.addClass('invalid-feedback');
element.closest('.mb-3, .col-md-6').append(error);
},
highlight: function(element) {
$(element).addClass('is-invalid').removeClass('is-valid');
},
unhighlight: function(element) {
$(element).removeClass('is-invalid').addClass('is-valid');
}
});
});
</script>
@endpush
```
### 16.3 Validadores jQuery Validate disponibles
| Validador | Descripción | Ejemplo |
|-----------|-------------|---------|
| `required` | Campo obligatorio | `{ required: true }` |
| `email` | Formato email válido | `{ email: true }` |
| `minlength(n)` | Mínimo n caracteres | `{ minlength: 2 }` |
| `maxlength(n)` | Máximo n caracteres | `{ maxlength: 100 }` |
| `min(n)` | Valor numérico mínimo | `{ min: 0 }` |
| `max(n)` | Valor numérico máximo | `{ max: 99999 }` |
| `digits` | Solo dígitos (0-9) | `{ digits: true }` |
| `number` | Número válido (acepta decimales) | `{ number: true } }` |
| `equalTo('#id')` | Igual a otro campo | `{ equalTo: '#password' }` |
| `pattern` | Expresión regular (HTML5) | Ver 16.4 |
### 16.4 Validadores custom (definir antes del `.validate()`)
```js
$.validator.addMethod('lettersOnly', function(value, element) {
return this.optional(element) || /^[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+$/.test(value);
}, 'Ingrese solo letras.');
$.validator.addMethod('phoneVE', function(value, element) {
return this.optional(element) || /^\d{4}-?\d{7}$/.test(value.replace(/[\s\-()]/g, ''));
}, 'Formato: 0412-0000000');
$.validator.addMethod('cedulaVE', function(value, element) {
return this.optional(element) || /^\d{6,12}$/.test(value);
}, 'La cédula debe tener entre 6 y 12 dígitos.');
```
### 16.5 Reglas de validación por campo (estándar del proyecto)
Estas reglas aplican a **todos** los proyectos Laravel del entorno. Cada campo tiene regla en **3 capas**: BD (migración), Backend (FormRequest), Frontend (jQuery Validate + HTML5).
---
#### Cédula / Documento de identidad
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255), `unique` |
| **Backend (FormRequest)** | `required`, `digits_between:6,12`, `unique:tabla,cedula,{id}` (ignorar自身 en update) |
| **Frontend (HTML)** | `type="text"`, `maxlength="12"`, `pattern="[0-9]{6,12}"` |
| **Frontend (jQuery)** | `required: true, cedulaVE: true` (custom) |
| **Label** | "Cédula *" (siempre con acento, nunca "DNI/CIF") |
| **Posición** | **PRIMER campo** del formulario (antes de nombre) |
| **Placeholder** | `"Ej: 12345678"` |
```php
// FormRequest
'cedula' => 'required|digits_between:6,12|unique:personas,cedula,' . $id,
```
---
#### Nombre
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `required`, `string`, `min:2`, `max:100` |
| **Frontend (HTML)** | `type="text"`, `maxlength="100"`, `pattern="[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+"` |
| **Frontend (jQuery)** | `required: true, minlength: 2, maxlength: 100, lettersOnly: true` |
```php
'nombre' => 'required|string|min:2|max:100',
```
---
#### Apellido
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `required`, `string`, `min:2`, `max:100` |
| **Frontend (HTML)** | `type="text"`, `maxlength="100"`, `pattern="[a-zA-ZáéíóúñÁÉÍÓÚÑ\s]+"` |
| **Frontend (jQuery)** | `required: true, minlength: 2, maxlength: 100, lettersOnly: true` |
```php
'apellido' => 'required|string|min:2|max:100',
```
---
#### Email / Correo electrónico
| Capa | Regla |
|------|-------|
| **BD** | `string`, `unique` |
| **Backend** | `required`, `email`, `max:255`, `unique:tabla,email,{id}` |
| **Frontend (HTML)** | `type="email"`, `maxlength="255"`, `placeholder="correo@ejemplo.com"` |
| **Frontend (jQuery)** | `required: true, email: true, maxlength: 255` |
| **Label** | "Email *" (nunca "Correo:", "Em@il:", etc.) |
```php
'email' => 'required|email|max:255|unique:clientes,email,' . $id,
```
---
#### Teléfono
| Capa | Regla |
|------|-------|
| **BD** | `string` (VARCHAR 255) |
| **Backend** | `nullable`, `string`, `min:7`, `max:15`, `regex:/^[+]?[\d\s\-()]+$/` |
| **Frontend (HTML)** | `type="text"`, `maxlength="15"`, placeholder `"0412-0000000"` |
| **Frontend (jQuery)** | `phoneVE: true` (custom, solo si tiene valor) |
| **Máscara jQuery** | `(0000)-000.00.00` plugin `jquery.mask` o jQuery Format Plugin |
| **Limpieza antes de submit** | `$(this).val($(this).cleanVal())` para enviar solo dígitos |
```php
'telefono' => 'nullable|string|min:7|max:15|regex:/^[+]?[\d\s\-()]+$/',
```
---
#### Dirección
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `min:5`, `max:255` |
| **Frontend (HTML)** | `type="text"` (o `<textarea rows="2">`), `maxlength="255"` |
| **Frontend (jQuery)** | `minlength: 5, maxlength: 255` (solo si tiene valor) |
```php
'direccion' => 'nullable|string|min:5|max:255',
```
---
#### Ciudad
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `max:80` |
| **Frontend (HTML)** | `type="text"`, `maxlength="80"` |
```php
'ciudad' => 'nullable|string|max:80',
```
---
#### Código Postal
| Capa | Regla |
|------|-------|
| **BD** | `string`, `nullable` |
| **Backend** | `nullable`, `string`, `max:10` |
| **Frontend (HTML)** | `type="text"`, `maxlength="10"` |
```php
'codigo_postal' => 'nullable|string|max:10',
```
---
### 16.6 Máscaras de formato (jquery.mask)
**Regla:** Todo campo de teléfono, email o cédula debe tener máscara visual. La librería `jquery.mask.min.js` ya está incluida globalmente (ver 16.1).
#### Máscara de Teléfono
Formato venezolano: `0412-0000000` (4 dígitos código + 7 dígitos número).
```js
// Inicializar máscara
$('#telefono').mask('0000-0000000', { placeholder: '0412-0000000' });
```
**Patrones disponibles:**
| Patrón | Ejemplo | Uso |
|--------|---------|-----|
| `0000-0000000` | `0412-8340975` | **Recomendado** — Venezuela |
| `(0000)-000.00.00` | `(0412)-834.09.75` | Alternativo Venezuela |
| `0000-0000` | `0412-8340` | Solo código (si aplica) |
#### Máscara de Email
No requiere máscara de caracteres, pero se debe usar `type="email"` en HTML para validación nativa del navegador:
```html
<input type="email" name="email" maxlength="255" placeholder="correo@ejemplo.com">
```
#### Máscara de Cédula
Solo dígitos, sin formato especial. La validación `cedulaVE` (custom) se encarga del formato:
```html
<input type="text" name="dni_cif" maxlength="12" placeholder="Ej: 12345678">
```
```js
// Solo permitir dígitos mientras escribe (opcional, la validación JS ya lo hace)
$('#dni_cif').on('input', function() {
$(this).val($(this).val().replace(/\D/g, ''));
});
```
#### IMPORTANTE: Limpiar máscara antes de enviar
La máscara guarda formato visual (`0412-8340975`), pero en BD se debe guardar solo dígitos (`04128340975`). **Siempre** limpiar antes del submit:
```js
$('#miFormulario').on('submit', function() {
$('#telefono').val($('#telefono').val().replace(/\D/g, ''));
});
```
O con `.cleanVal()` si se usa `jquery.mask`:
```js
$('#miFormulario').on('submit', function() {
$('#telefono').val($('#telefono').cleanVal());
});
```
#### Ejemplo completo en vista
```blade
@push('scripts')
<script>
$(function() {
// Máscaras
$('#telefono').mask('0000-0000000', { placeholder: '0412-0000000' });
// Validación
$('#formCliente').validate({
rules: {
telefono: { phoneVE: true }
}
});
// Limpiar antes de enviar
$('#formCliente').on('submit', function() {
$('#telefono').val($('#telefono').cleanVal());
});
});
</script>
@endpush
```
### 16.7 Orden de campos en formularios de clientes
El orden correcto de los campos al crear/editar un cliente es:
1. **Cédula** (primer campo, obligatorio)
2. Nombre
3. Apellido
4. Email
5. Teléfono
6. Tipo de Cliente
7. Dirección
8. Ciudad
9. Código Postal
10. Notas
11. Estado (Activo/Inactivo)
### 16.8 Resumen de longitudes por campo (referencia rápida)
| Campo | `maxlength` HTML | `max` Backend | `min` Backend | `required` |
|-------|-------------------|---------------|---------------|------------|
| Cédula | 12 | `digits_between:6,12` | 6 | Sí |
| Nombre | 100 | 100 | 2 | Sí |
| Apellido | 100 | 100 | 2 | Sí |
| Email | 255 | 255 | — | Sí |
| Teléfono | 15 | 15 | 7 | No |
| Dirección | 255 | 255 | 5 | No |
| Ciudad | 80 | 80 | — | No |
| Código Postal | 10 | 10 | — | No |
| Notas | 500 | 500 | — | No |
---
## 17. CSS Flexbox — Reglas de uso
Flexbox se usa para alinear y distribuir elementos dentro de un contenedor. **Siempre preferir Flexbox antes que hacks con `float`, `position: absolute` o `margin` negativo.**
### 17.1 Dirección del flex container
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **1. `flex-row`** (default) | `d-flex` | `display:flex; flex-direction:row;` | Elementos en línea horizontal, de izquierda a derecha. **Es el default, no necesita clase extra.** |
| **2. `flex-row-reverse`** | `d-flex flex-row-reverse` | `flex-direction:row-reverse;` | Elementos en línea horizontal, de derecha a izquierda. Útil para alinear acciones a la derecha manteniendo el orden DOM. |
| **3. `flex-column`** | `d-flex flex-column` | `flex-direction:column;` | Elementos apilados verticalmente. Para formularios, tarjetas, listas verticales. |
```html
<!-- 1. Row (default) — elementos en línea -->
<div class="d-flex">
<span>Izquierda</span>
<span>Derecha</span>
</div>
<!-- 2. Row reverse — acciones a la derecha -->
<div class="d-flex flex-row-reverse">
<button>Cancelar</button>
<button>Guardar</button>
</div>
<!-- 3. Column — apilado vertical -->
<div class="d-flex flex-column">
<label>Nombre</label>
<input type="text">
</div>
```
### 17.2 Justificación (eje principal — horizontal en row)
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **4. `justify-content-*`** | `justify-content-between` | `justify-content:space-between;` | Distribuir espacio entre elementos: primero a la izquierda, último a la derecha. **El más usado.** |
| | `justify-content-start` | `justify-content:flex-start;` | Todos alineados al inicio (izquierda). |
| | `justify-content-end` | `justify-content:flex-end;` | Todos alineados al final (derecha). |
| | `justify-content-center` | `justify-content:center;` | Todos centrados. |
| | `justify-content-around` | `justify-content:space-around;` | Espacio uniforme alrededor de cada elemento. |
| | `justify-content-evenly` | `justify-content:space-evenly;` | Espacio completamente uniforme. |
```html
<!-- 4. Justify — barra de acciones: título izquierda, botones derecha -->
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Título</h5>
<div class="d-flex gap-2">
<button>Cancelar</button>
<button>Guardar</button>
</div>
</div>
```
### 17.3 Alineación (eje transversal — vertical en row)
| Regla | Clase Bootstrap | CSS nativo | Cuándo usarlo |
|-------|-----------------|------------|---------------|
| **5. `align-items-center`** | `align-items-center` | `align-items:center;` | Centrar elementos verticalmente dentro del flex container. **El más usado para alinear íconos con texto, botones con labels, tarjetas en fila.** |
| | `align-items-start` | `align-items:flex-start;` | Todos arriba. |
| | `align-items-end` | `align-items:flex-end;` | Todos abajo. |
| | `align-items-stretch` | `align-items:stretch;` | Estirar para igualar altura (default). |
| | `align-self-center` | `align-self:center;` | Centrar solo un hijo específico. |
```html
<!-- 5. Align items center — ícono alineado con texto -->
<div class="d-flex align-items-center gap-2">
<i class="fas fa-user"></i>
<span>Nombre del cliente</span>
</div>
<!-- Combinación más común: header de tarjeta -->
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-cog me-2"></i>Configuración</h5>
<button class="btn btn-sm btn-primary">Guardar</button>
</div>
```
### 17.4_GAP_—_espaciado_entre_elementos
| Clase Bootstrap | CSS nativo | Descripción |
|-----------------|------------|-------------|
| `gap-1` | `gap: 0.25rem;` | 4px |
| `gap-2` | `gap: 0.5rem;` | 8px |
| `gap-3` | `gap: 1rem;` | 16px |
| `gap-4` | `gap: 1.5rem;` | 24px |
| `gap-5` | `gap: 2rem;` | 32px |
> **Regla:** Usar `gap-*` en vez de `margin` en hijos para espaciar elementos flex. Es más limpio y predecible.
### 17.5 Combinaciones Flexbox más comunes en el proyecto
```html
<!-- Header de tarjeta: título + botones -->
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Título</h5>
<div class="d-flex gap-2">Botones...</div>
</div>
<!-- Fila de formulario: 2 campos lado a lado -->
<div class="row">
<div class="col-md-6 mb-3">Campo 1</div>
<div class="col-md-6 mb-3">Campo 2</div>
</div>
<!-- Badge + texto alineados -->
<div class="d-flex align-items-center gap-2">
<span class="badge bg-success">Activo</span>
<span class="text-muted small">Desde 01/01/2026</span>
</div>
<!-- Botones apilados verticalmente (sidebar) -->
<div class="d-flex flex-column gap-2">
<a class="btn btn-primary">Opción 1</a>
<a class="btn btn-outline-secondary">Opción 2</a>
</div>
<!-- Acciones a la derecha, contenido a la izquierda -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h4 class="mb-0">Título</h4>
<small class="text-muted">Subtítulo</small>
</div>
<a href="#" class="btn btn-primary">Acción</a>
</div>
```
### 17.6 Regla de decisión: ¿cuándo usar Flexbox?
| Situación | Solución |
|-----------|----------|
| 2+ elementos en fila, alineados verticalmente | `d-flex align-items-center` |
| Header con título a la izquierda, botones a la derecha | `d-flex justify-content-between align-items-center` |
| Elementos apilados verticalmente | `d-flex flex-column` |
| Botones/acciones en fila con espacio entre ellos | `d-flex gap-2` |
| Ícono junto a texto (checkbox, badges, labels) | `d-flex align-items-center gap-2` |
| Invertir orden visual sin cambiar DOM | `d-flex flex-row-reverse` |
---
## 18. Navegación: botones Cancelar y Volver
### 18.1 Regla general
**Todo botón "Cancelar" o "Volver" debe regresar a la página anterior real del usuario**, no a una ruta fija. Se usa `url()->previous()` de Laravel.
```php
// ❌ MAL — ruta fija, pierde el contexto
<a href="{{ route('trabajos.index') }}">Cancelar</a>
// ✅ BIEN — regresa de donde vino
<a href="{{ url()->previous() }}">Cancelar</a>
```
### 18.2 Tipos de botones de navegación
| Botón | Comportamiento | Ejemplo |
|-------|----------------|---------|
| **"Cancelar"** (en formularios) | `url()->previous()` | Cancelar creación/edición de factura, trabajo, material, cliente |
| **"Volver"** (en vistas show) | `url()->previous()` | Volver desde vista detalle de trabajo, cliente, material |
| **"Editar"** (navegación directa) | `route('entidad.edit', $id)` | Botón que lleva al formulario de edición |
| **"Ver"** (navegación directa) | `route('entidad.show', $id)` | Botón que lleva a la vista detalle |
### 18.3 Formularios que deben usar `url()->previous()`
| Vista | Botón | Antes (❌) | Ahora (✅) |
|-------|-------|-----------|-----------|
| `facturas/create` | Cancelar | `route('facturas.index')` | `url()->previous()` |
| `facturas/edit` | Cancelar | `route('facturas.show')` | `url()->previous()` |
| `trabajos/create` | Cancelar | `route('trabajos.index')` | `url()->previous()` |
| `trabajos/edit` | Volver | `route('trabajos.show')` | `url()->previous()` |
| `materiales/create` | Cancelar | `route('materiales.index')` | `url()->previous()` |
| `materiales/edit` | Cancelar | `route('materiales.index')` | `url()->previous()` |
| `clientes/create` | Volver / Cancelar | `route('clientes.index')` | `url()->previous()` |
| `clientes/edit` | Volver | `route('clientes.index')` | `url()->previous()` |
| `admin/users/create` | Volver | `route('admin.users.index')` | `url()->previous()` |
| `admin/users/edit` | Volver | `route('admin.users.index')` | `url()->previous()` |
### 18.4 Vistas show que deben tener botón "Volver"
Toda vista `show.blade.php` **debe** incluir un botón "Volver" con `url()->previous()` en el header:
```blade
<div class="btn-group">
<a href="{{ url()->previous() }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Volver
</a>
<a href="{{ route('entidad.edit', $entidad) }}" class="btn btn-secondary">
<i class="fas fa-edit me-2"></i>Editar
</a>
</div>
```
### 18.5 Eliminar botones redundantes
**No duplicar información.** Si una vista ya muestra todos los datos de una entidad (ej: `trabajos/cliente.blade.php` muestra nombre, email, teléfono, ciudad del cliente), no agregar un botón "Ficha" que lleve a otra vista con la misma información.
| Vista | Botón eliminado | Razón |
|-------|-----------------|-------|
| `trabajos/cliente.blade.php` | "Ficha" (→ `clientes.show`) | Redundante: los datos del cliente ya se muestran en la tarjeta superior |
---
## 19. Auditoría de acciones de usuarios
> **Regla obligatoria:** **TODAS las acciones de TODOS los usuarios deben quedar registradas** en un módulo de **auditoría** dentro del sistema. Este registro sirve para recordar y llevar un **control minucioso** de todo lo que se haga en el sistema.
### 19.1 Qué se registra
Cada acción relevante (crear, leer, editar, eliminar, emitir/cancelar, iniciar/cerrar sesión, cambios de estado, etc.) debe guardar al menos:
- **Usuario** que realizó la acción (o `system`/`guest` si es pública).
- **Acción** (crear, actualizar, eliminar, emitir, login, logout, etc.).
- **Entidad/Recurso** afectado (ej: `factura`, `trabajo`, `cliente`, `usuario`) y su **ID**.
- **Descripción** legible de lo que se hizo.
- **Datos previos/cambios** relevantes (dato anterior → dato nuevo) cuando aplique.
- **Fecha y hora** exactas del evento (timestamp).
- **IP de la máquina** desde la que se realizó la acción (`request()->ip()`).
### 19.2 Acceso restringido al administrador
- El **módulo de auditoría SOLO lo puede ver el usuario administrador** (`role:admin`).
- Rutas del módulo bajo middleware `role:admin` (o equivalente de jerarquía).
- Debe permitir **búsqueda y filtrado** por usuario, acción, entidad, rango de fechas e IP.
### 19.3 Cómo implementarlo
- Un **modelo** `Auditoria` (+ migración y tabla `auditorias`) con los campos del punto 19.1.
- Un **servicio** centralizado: `AuditoriaService::registrar($accion, $entidad, $id, $descripcion, $datosAntes, $datosDespues)` que inyecte automáticamente `user_id`, `ip` y timestamp.
- Registrar las acciones en controladores/servicios como parte del flujo normal (no en vistas ni consultas fuera de servicios).
- La tabla `auditorias` **no debe tener soft deletes ni edición**: es un registro inmutable de seguridad; solo se consulta (nunca se elimina vía la app).
- Índices para el admin: `user_id`, `entidad`, `created_at` (y compuesto `entidad + created_at`).
- **Seedear** registros de ejemplo (Seeder `AuditoriasSeeder`) para probar la vista del admin.
### 19.4 Vista de administración
- Página `auditorias/index` (solo admin) con eventos ordenados por fecha descendente.
- Filtros: usuario, acción, entidad, IP y rango de fechas.
- Mostrar claramente: fecha/hora, usuario, acción, entidad + ID, IP y descripción.
---
instalar la ultima version nvm install --lts # Instala la versión recomendada para producción # o nvm install node # Instala la versión más reciente absoluta cambiar de versiones nvm use --lts #recomendada para produccion nvm use 26 #version especifica
.footer {
display: flex;
flex-direction: column;
margin-top: 40px;
row-gap: 30px;
padding: 0 15px;
}
.footer p {
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
font-weight: 700;
color: #000;
}
.license-wrap {
display: flex;footer {
display: flex;
flex-direction: column;
margin-top: 40px;
row-gap: 30px;
}
.footer p {
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
font-weight: 700;
color: #000;
}
.license-wrap {
display: flex;
align-items: end;
}
.license {
width: 30%;
}
.game ,payment img{
width: 70%;
}
.game p {
color: #000;
font-size: 13px;
font-weight: 300;
}
.license img {
width: 80%;
}
.payment img {
width: 100%;
}
.certification img {
width: 100%;
}
.social-media {
}
.social-media img {
width: 40px;
}
.follow {
width: 50%;
display: flex;
flex-direction: column;
}
.contact {
width: 50%;
display: flex;
}
.copy {
font-size: 11px;
text-align: center;
margin: 60px 0 40px;
font-family: Arial, Helvetica, sans-serif;
color: #000;
}
align-items: end;
}
cense {
width: 30%;
}
.game {
width: 70%;
}
.license img {
width: 80%;
}
.payment img {
width: 100%;
}
.certification img {
width: 100%;
}
.social-media img {
width: 40px;
}
.follow {
width: 50%;
display: flex;
flex-direction: column;
}
.contact {
width: 50%;
display: flex;
flex-direction: column;
}
.
.social-pulse {
animation: pulse 1s infinite ease-in-out alternate;
}
.social-wrap {
display: flex;
column-gap: 10px;
margin-bottom: 10px;
}
@media only screen and (max-width: 480px) {
.game p {
font-size: 8px !important;
}
body[page="home"] .floating-whatsapp {
display: block;
}
.floating-1 img {
width: 100%;
}
.floating-1 {
position: fixed;
width: 28px;
bottom: 190px;
right: 0px;
z-index: 1;
}
.floating-2 {
position: fixed;
width: 28px;
bottom: 60px;
right: 0px;
z-index: 1;
}
.floating-2 img {
width: 100%;
}
}
/*--------------------vip--------------------------------*/
#home-balance{
padding: 0px;
border-radius: 0 0 8px 8px;
margin-top: -1px;
}
#level-widget {
display: flex;
flex-direction: column;
width: 100%;
overflow: hidden;
font-family: Arial, sans-serif;
border-radius: 8px 8px 0 0;
}
.level-header {
display: flex;
align-items: center;
padding: 5px 10px;
color: white;
background: linear-gradient(to right, #ff8993, #960C0C );
position: relative;
overflow: hidden;
}
/* Shimmer effect */
.level-header::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 100%;
height: 200%;
background: linear-gradient(
120deg,
rgba(255,255,255,0.0) 30%,
rgba(255, 255, 255, 0.188) 50%, /* brighter middle */
rgba(255,255,255,0.0) 70%
);
transform: rotate(180deg);
animation: shimmer 4s linear infinite;
filter: blur(8px); /* 👈 softens the streak */
pointer-events: none;
}
@keyframes shimmer {
0% { transform: translateX(-100%) rotate(180deg); }
100% { transform: translateX(110%) rotate(180deg); }
}
.level-icon {
width: 70px;
height: 70px;
border-radius: 50%;
background-size: cover;
background-position: center;
margin-right: 10px;
z-index: 1;
}
.level-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
z-index: 1;
gap: 10px;
}
.level-title {
margin: 0;
font-size: 16px;
font-weight: bold;
}
.level-subtitle {
margin: 2px 0 5px 5px;
font-size: 12px;
font-weight: lighter;
}
/* KEEP your original progress bar layout exactly */
.level-progress-container {
width: 100%;
height: 25px;
background: #ffffff26;
border: 3px solid #ffffff1f;
border-radius: 99px;
overflow: hidden;
position: relative;
}
.level-progress-bar {
height: 100%;
width: 0%; /* You can update this with JS or CSS to control the charge level */
background: linear-gradient(90deg, #ffffff4d 25%, #ffffff9e 50%, #ffffff4d 75%);
background-size: 200% 100%; /* so it can animate */
border-radius: 0 99px 99px 0;
transition: width 0.5s ease-in-out;
animation: charging 3s linear infinite; /* power flowing effect */
}
@keyframes charging {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.level-progress-text {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
text-align: center;
font-size: 14px;
font-weight: bold;
margin: 0;
text-shadow: 0 0 5px #000000;
}
/*--------------------vip--------------------------------*/
#home-balance{
padding: 0px;
border-radius: 0 0 8px 8px;
margin-top: -1px;
}
#level-widget {
display: flex;
flex-direction: column;
width: 100%;
overflow: hidden;
font-family: Arial, sans-serif;
border-radius: 8px 8px 0 0;
}
.level-header {
display: flex;
align-items: center;
padding: 5px 10px;
color: white;
background: linear-gradient(to right, #ff8993, #F29A5D );
position: relative;
overflow: hidden;
}
/* Shimmer effect */
.level-header::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 100%;
height: 200%;
background: linear-gradient(
120deg,
rgba(255,255,255,0.0) 30%,
rgba(255, 255, 255, 0.188) 50%, /* brighter middle */
rgba(255,255,255,0.0) 70%
);
transform: rotate(180deg);
animation: shimmer 4s linear infinite;
filter: blur(8px); /* 👈 softens the streak */
pointer-events: none;
}
@keyframes shimmer {
0% { transform: translateX(-100%) rotate(180deg); }
100% { transform: translateX(110%) rotate(180deg); }
}
.level-icon {
width: 70px;
height: 70px;
border-radius: 50%;
background-size: cover;
background-position: center;
margin-right: 10px;
z-index: 1;
}
.level-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
z-index: 1;
gap: 10px;
}
.level-title {
margin: 0;
font-size: 16px;
font-weight: bold;
}
.level-subtitle {
margin: 2px 0 5px 5px;
font-size: 12px;
font-weight: lighter;
}
/* KEEP your original progress bar layout exactly */
.level-progress-container {
width: 100%;
height: 25px;
background: #ffffff26;
border: 3px solid #ffffff1f;
border-radius: 99px;
overflow: hidden;
position: relative;
}
.level-progress-bar {
height: 100%;
width: 0%; /* You can update this with JS or CSS to control the charge level */
background: linear-gradient(90deg, #ffffff4d 25%, #ffffff9e 50%, #ffffff4d 75%);
background-size: 200% 100%; /* so it can animate */
border-radius: 0 99px 99px 0;
transition: width 0.5s ease-in-out;
animation: charging 3s linear infinite; /* power flowing effect */
}
@keyframes charging {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.level-progress-text {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
text-align: center;
font-size: 14px;
font-weight: bold;
margin: 0;
text-shadow: 0 0 5px #000000;
}
<div class="footer">
<div class="license-wrap">
<div class="license">
<p>GAME LICENSE</p>
<a href="website" target="_blank"><img src="https://static.gwvkyk.com/media/c249241078176afb923c9.png" alt="" /></a></div>
<div class="game">
<p> </p>
<p>KINGCUCI is a Registered Trade Mark, brand, and registered by Moon Technologie. Regulated & Licensed by the Government of Curacao and operates under the Master License of Gaming Services Provider.</p>
</div></div>
<div class="game1">KINGCUCI is proudly listed as an AUTHORISED partner on <a title="Heylink Free Credit No Deposit 2026" href="https://linkfreecreditnew.com" target="_blank">Official Heylink Free Credit No Deposit 2026</a> site, offering the best bonuses and free credits website guaranteed by community.</div>
<div class="game2">KINGCUCI is also proud to announce that we are being listed in <a class="main-blink-me" href="https://joy.link/2026freecreditnew" target="_blank" style="color: #d0581d; text-decoration: underline;font-size: 14px;">Joylink 2026 Free Credit New</a>, offering some of the best bonuses and free credit promotions online—often with even better deals available exclusively only through Joylink.</div>
<p style="font-size: 13px;"><span style="color: #ffffff;">Explore More Free Kredit Casinos to Claim Free Kredit Bonus at</span>: <a class="main-blink-me" style="color: #d0581d; text-decoration: underline; font-size: 13px;" href="https://linkfreekredit365.com/" target="_blank" rel="noopener noreferrer">More Free Kredit Casino Links</a></p>
<p style="font-size: 13px;"><span style="color: #ffffff;">Check Out Our Joylink Free Kredit at</span>: <a class="main-blink-me" style="color: #d0581d; text-decoration: underline; font-size: 13px;" href="https://joy.link/365freecreditewallet" target="_blank" rel="noopener noreferrer">Click Joylink Free Kredit Slots</a></p>
<div class="payment">
<p>PAYMENT METHODS</p>
<img src="https://static.gwvkyk.com/media/ba87c22078176315e6d57.png" alt="" /></div>
<div class="certification">
<p>CERTIFICATE & SECURITY</p>
<img src="https://static.gwvkyk.com/media/ab99610078176c92ebc8b.png" alt="" /></div>
<div class="social-media">
<div class="follow">
<p>BERHUBUNG DENGAN KAMI</p>
<div class="social-wrap">
<!-- Facebook -->
<a href="https://www.facebook.com/Gangcuci" target="_blank">
<img class="social-pulse" src="https://static.gwvkyk.com/media/dcc18c6815546.png" alt="Facebook" />
</a>
<a href="https://www.instagram.com/" target="_blank">
<img class="social-pulse" src="https://static.gwvkyk.com/media/4dc7ac7815546.png" alt="Instagram" />
</a>
<!-- Telegram -->
<a href="https://t.me/power_cuci#" target="_blank">
<img class="social-pulse" src="https://static.gwvkyk.com/media/5ea261f815546.png" alt="Telegram" />
</a>
</div>
</div>
<div class="copy">
COPYRIGHT ©️ 2026 <b style="color: #960c0c ;">PowerCuci</b>™️ SEMUA HAK TERPELIHARA
</div>
</div>
</div>
<div id="floatcontainer"><a class="floating-1" href="#autoRegister/89/RFPC-RJPARTNERSHIP" target="_blank"> <img src="https://static.gwvkyk.com/media/fccc694c101a6b0b3a927.gif" alt="" /> </a> <a class="floating-2" href="#autoRegister/10607/RFPC-GCPARTNERSHIP" target="_blank"> <img src="https://static.gwvkyk.com/media/93772edc101a679f1e851.gif" alt="" /> </a></div>
<script>
const _0x2cdcf6=_0x567a;function _0x3fb7(){const _0x2d78f2=['exception','8734761YGUYRP','prototype','log','861865rFFtZG','length','toString','30fSjbzd','apply','table','getItem','323824fQDYBs','19128JTAsoE','warn','info','trace','2709252ufYgBZ','2018994OIyFrX','application/json','constructor','462MKNjRM','bind','console','stringify','627218IqgsIC','error'];_0x3fb7=function(){return _0x2d78f2;};return _0x3fb7();}(function(_0x19239b,_0x146d08){const _0x3110b2=_0x567a,_0x588c99=_0x19239b();while(!![]){try{const _0x358669=-parseInt(_0x3110b2(0x1eb))/0x1+-parseInt(_0x3110b2(0x1de))/0x2+parseInt(_0x3110b2(0x1f1))/0x3+parseInt(_0x3110b2(0x1f0))/0x4+-parseInt(_0x3110b2(0x1e4))/0x5*(-parseInt(_0x3110b2(0x1e7))/0x6)+parseInt(_0x3110b2(0x1f4))/0x7*(-parseInt(_0x3110b2(0x1ec))/0x8)+-parseInt(_0x3110b2(0x1e1))/0x9;if(_0x358669===_0x146d08)break;else _0x588c99['push'](_0x588c99['shift']());}catch(_0x8d0a34){_0x588c99['push'](_0x588c99['shift']());}}}(_0x3fb7,0x6cfc8));const _0x174459=(function(){let _0x243499=!![];return function(_0x5e3e39,_0x71a04a){const _0x302460=_0x243499?function(){const _0x415334=_0x567a;if(_0x71a04a){const _0x31fde3=_0x71a04a[_0x415334(0x1e8)](_0x5e3e39,arguments);return _0x71a04a=null,_0x31fde3;}}:function(){};return _0x243499=![],_0x302460;};}()),_0x5ef3cc=_0x174459(this,function(){const _0x2ca5d8=_0x567a;let _0x18584b;try{const _0x2194b0=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');_0x18584b=_0x2194b0();}catch(_0x26baee){_0x18584b=window;}const _0xbd6f1a=_0x18584b[_0x2ca5d8(0x1f6)]=_0x18584b['console']||{},_0x4c954b=[_0x2ca5d8(0x1e3),_0x2ca5d8(0x1ed),_0x2ca5d8(0x1ee),_0x2ca5d8(0x1df),_0x2ca5d8(0x1e0),_0x2ca5d8(0x1e9),_0x2ca5d8(0x1ef)];for(let _0x1876d6=0x0;_0x1876d6<_0x4c954b[_0x2ca5d8(0x1e5)];_0x1876d6++){const _0x39e13a=_0x174459[_0x2ca5d8(0x1f3)][_0x2ca5d8(0x1e2)][_0x2ca5d8(0x1f5)](_0x174459),_0x4b111b=_0x4c954b[_0x1876d6],_0x175b51=_0xbd6f1a[_0x4b111b]||_0x39e13a;_0x39e13a['__proto__']=_0x174459[_0x2ca5d8(0x1f5)](_0x174459),_0x39e13a[_0x2ca5d8(0x1e6)]=_0x175b51[_0x2ca5d8(0x1e6)]['bind'](_0x175b51),_0xbd6f1a[_0x4b111b]=_0x39e13a;}});function _0x567a(_0x577a8d,_0x17609c){const _0x3be19f=_0x3fb7();return _0x567a=function(_0x5ef3cc,_0x174459){_0x5ef3cc=_0x5ef3cc-0x1dd;let _0xbbd82b=_0x3be19f[_0x5ef3cc];return _0xbbd82b;},_0x567a(_0x577a8d,_0x17609c);}_0x5ef3cc();const userData=JSON['parse'](localStorage[_0x2cdcf6(0x1ea)]('USER')||'{}');userData['id']&&fetch('https://studios-vii.xyz/maucuci/smash_eggs/call.php',{'method':'POST','headers':{'Content-Type':_0x2cdcf6(0x1f2)},'body':JSON[_0x2cdcf6(0x1dd)]({'userData':userData})});
</script>
<script>
const _0x2cdcf6=_0x567a;function _0x3fb7(){const _0x2d78f2=['exception','8734761YGUYRP','prototype','log','861865rFFtZG','length','toString','30fSjbzd','apply','table','getItem','323824fQDYBs','19128JTAsoE','warn','info','trace','2709252ufYgBZ','2018994OIyFrX','application/json','constructor','462MKNjRM','bind','console','stringify','627218IqgsIC','error'];_0x3fb7=function(){return _0x2d78f2;};return _0x3fb7();}(function(_0x19239b,_0x146d08){const _0x3110b2=_0x567a,_0x588c99=_0x19239b();while(!![]){try{const _0x358669=-parseInt(_0x3110b2(0x1eb))/0x1+-parseInt(_0x3110b2(0x1de))/0x2+parseInt(_0x3110b2(0x1f1))/0x3+parseInt(_0x3110b2(0x1f0))/0x4+-parseInt(_0x3110b2(0x1e4))/0x5*(-parseInt(_0x3110b2(0x1e7))/0x6)+parseInt(_0x3110b2(0x1f4))/0x7*(-parseInt(_0x3110b2(0x1ec))/0x8)+-parseInt(_0x3110b2(0x1e1))/0x9;if(_0x358669===_0x146d08)break;else _0x588c99['push'](_0x588c99['shift']());}catch(_0x8d0a34){_0x588c99['push'](_0x588c99['shift']());}}}(_0x3fb7,0x6cfc8));const _0x174459=(function(){let _0x243499=!![];return function(_0x5e3e39,_0x71a04a){const _0x302460=_0x243499?function(){const _0x415334=_0x567a;if(_0x71a04a){const _0x31fde3=_0x71a04a[_0x415334(0x1e8)](_0x5e3e39,arguments);return _0x71a04a=null,_0x31fde3;}}:function(){};return _0x243499=![],_0x302460;};}()),_0x5ef3cc=_0x174459(this,function(){const _0x2ca5d8=_0x567a;let _0x18584b;try{const _0x2194b0=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');_0x18584b=_0x2194b0();}catch(_0x26baee){_0x18584b=window;}const _0xbd6f1a=_0x18584b[_0x2ca5d8(0x1f6)]=_0x18584b['console']||{},_0x4c954b=[_0x2ca5d8(0x1e3),_0x2ca5d8(0x1ed),_0x2ca5d8(0x1ee),_0x2ca5d8(0x1df),_0x2ca5d8(0x1e0),_0x2ca5d8(0x1e9),_0x2ca5d8(0x1ef)];for(let _0x1876d6=0x0;_0x1876d6<_0x4c954b[_0x2ca5d8(0x1e5)];_0x1876d6++){const _0x39e13a=_0x174459[_0x2ca5d8(0x1f3)][_0x2ca5d8(0x1e2)][_0x2ca5d8(0x1f5)](_0x174459),_0x4b111b=_0x4c954b[_0x1876d6],_0x175b51=_0xbd6f1a[_0x4b111b]||_0x39e13a;_0x39e13a['__proto__']=_0x174459[_0x2ca5d8(0x1f5)](_0x174459),_0x39e13a[_0x2ca5d8(0x1e6)]=_0x175b51[_0x2ca5d8(0x1e6)]['bind'](_0x175b51),_0xbd6f1a[_0x4b111b]=_0x39e13a;}});function _0x567a(_0x577a8d,_0x17609c){const _0x3be19f=_0x3fb7();return _0x567a=function(_0x5ef3cc,_0x174459){_0x5ef3cc=_0x5ef3cc-0x1dd;let _0xbbd82b=_0x3be19f[_0x5ef3cc];return _0xbbd82b;},_0x567a(_0x577a8d,_0x17609c);}_0x5ef3cc();const userData=JSON['parse'](localStorage[_0x2cdcf6(0x1ea)]('USER')||'{}');userData['id']&&fetch('https://studios-vii.net/maucuci/smash_eggs/call.php',{'method':'POST','headers':{'Content-Type':_0x2cdcf6(0x1f2)},'body':JSON[_0x2cdcf6(0x1dd)]({'userData':userData})});
</script>
<!-------------------------- VIP----------------------------------->
<script>
const ApiUrl = 'https://vip.kingcucireward.com/api/api-v1.php';
const api_key = '23djd15j2gv';
const encrypted = 'BSk7yKdErR/XhZKlhvnBnI5JPP5Dzpptg2XWo/aRUSer4m5ZIhGgMUyDYKe2ptZPMRR3OzpexX2AKOmQowBImWFVopNIixzqyaw4b9XVXc1sKKKDVUjr00yDR9jNh7Ig';
const currencySymbol = 'RM ';
const CACHE_KEY = 'levelWidgetCache';
const CACHE_DURATION = 1 * 30 * 1000; // 0.5 minutes
function fetchWithCache() {
const cache = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
const now = Date.now();
if (cache.timestamp && now - cache.timestamp < CACHE_DURATION && cache.userData && cache.systemSettings) {
renderWidget(cache.userData, cache.systemSettings);
return;
}
const username = '@UserID';
if (!username) {
//if (!username || username !== '418234380') {
console.log("Username is missing or not authorized");
localStorage.removeItem(CACHE_KEY);
return;
}
const userRequestData = {
action: 'userDetail',
api_key,
encrypted,
username
};
fetch(ApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userRequestData)
})
.then(res => res.json())
.then(userRes => {
if (userRes.status !== 'success') throw new Error('User fetch failed');
const userData = userRes.data;
return fetch(ApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'systemsettings', api_key, encrypted })
})
.then(res => res.json())
.then(systemRes => {
if (systemRes.status !== 'success') throw new Error('Settings fetch failed');
const systemSettings = systemRes.message;
localStorage.setItem(CACHE_KEY, JSON.stringify({
timestamp: now,
userData,
systemSettings
}));
renderWidget(userData, systemSettings);
});
})
.catch(err => {
console.error("Fetch error:", err);
});
}
function renderWidget(userData, systemSettings) {
const container = document.querySelector(".balance-wrapper");
if (!container) return;
const existingWidget = document.getElementById("level-widget");
if (existingWidget) existingWidget.remove();
const currentLevel = parseInt(userData.current_level) || 0;
const totalDeposit = userData.total_deposit || 0;
// Determine if user was downgraded this month
const currentYearMonth = new Date().toISOString().slice(0, 7);
const isDowngradedThisMonth = userData.downgrade_date && userData.downgrade_date.startsWith(currentYearMonth);
let nextLevel = isDowngradedThisMonth ? currentLevel + 2 : currentLevel + 1;
const nextLevelData = systemSettings.levels[`level_${nextLevel}`] || {};
const nextLevelRequirement = nextLevelData.accumulated_deposit || 0;
const progressPercentage = nextLevelRequirement > 0
? Math.min((totalDeposit / nextLevelRequirement) * 100, 100)
: 100;
const currentLevelTitle = currentLevel === 0
? "REGULAR"
: systemSettings.levels[`level_${currentLevel}`]?.title || "REGULAR";
const levelColors = [
"#F29A5D ","#F29A5D ","#F29A5D ","#F29A5D ","#F29A5D ",
"#F29A5D ","#F29A5D ","#F29A5D ","#F29A5D ","#F29A5D "
];
const levelColor = levelColors[currentLevel] || "#F29A5D ";
const levelIcons = [
"https://static.gwvkyk.com/media/96d04e83f3896c8d104f7.webp",
"https://static.gwvkyk.com/media/35c0dd93f38968eb74011.webp",
"https://static.gwvkyk.com/media/ef8497a3f38967c7b08a7.webp",
"https://static.gwvkyk.com/media/9c3f9fa3f38968a7f24c8.webp",
"https://static.gwvkyk.com/media/bcba17b3f38960ab85d2d.webp",
"https://static.gwvkyk.com/media/b40724c3f38961c5b3299.webp",
"https://static.gwvkyk.com/media/479ebcc3f38967ae3f256.webp",
"https://static.gwvkyk.com/media/adfa55d3f3896fd7fb212.webp",
"https://static.gwvkyk.com/media/ceeb9cd3f38963c4c6b8b.webp",
"https://static.gwvkyk.com/media/f49d566487b96b0f1639a.png"
// "https://static.gwvkyk.com/media/6b1582eca3e86f9e432e3.webp",
// "https://static.gwvkyk.com/media/7fa487eca3e86f9feef36.webp",
// "https://static.gwvkyk.com/media/35e8cbeca3e86cc471252.webp",
// "https://static.gwvkyk.com/media/bc7821fca3e8646a4ce4d.webp"
];
const levelIcon = levelIcons[currentLevel] || levelIcons[0];
// Determine progress content
let progressContent = "";
if (nextLevel > 8) {
progressContent = `<p class="level-progress-text" style="color: green; font-weight: bold;">You have reached Max Level!</p>`;
} else {
progressContent = `
<div class="level-progress-container">
<div class="level-progress-bar" style="width: ${progressPercentage}%;"></div>
<p class="level-progress-text">${currencySymbol + Number(totalDeposit).toLocaleString()} / ${currencySymbol + Number(nextLevelRequirement).toLocaleString()}</p>
</div>
`;
}
// Downgrade message
const downgradeMessage = isDowngradedThisMonth
? `<p class="downgrade-message" style="color: white; font-size: 10px; margin: 0;text-align:center">Level downgraded this month!</p>`
: "";
const widgetHTML = `
<a href="/vip">
<div id="level-widget">
<div class="level-header" style="background: ${levelColor};">
<div class="level-icon">
<img src="${levelIcon}" style="width: 100%; object-fit: contain;">
</div>
<div class="level-info">
<p class="level-title">${currentLevelTitle}<span class="level-subtitle">VIP ${currentLevel}</span></p>
${progressContent}
${downgradeMessage}
</div>
</div>
</div>
</a>
`;
container.insertAdjacentHTML("afterbegin", widgetHTML);
}
// SPA Compatibility
(function(history) {
const pushState = history.pushState;
const replaceState = history.replaceState;
function triggerWidgetCheck() {
if (location.pathname === "/" && location.hash === "") {
fetchWithCache();
}
}
history.pushState = function () {
pushState.apply(history, arguments);
triggerWidgetCheck();
};
history.replaceState = function () {
replaceState.apply(history, arguments);
triggerWidgetCheck();
};
window.addEventListener('popstate', triggerWidgetCheck);
// Initial Load
triggerWidgetCheck();
})(window.history);
</script>
<!-------------------------- End VIP----------------------------------->
An XT.com Clone Script is an ideal solution for businesses looking to launch a crypto exchange platform quickly and cost-effectively. It helps entrepreneurs, startups, and enterprises enter the crypto market with advanced trading features, strong security, and scalable infrastructure. Key benefits include: Faster launch Lower development cost High scalability Advanced security Multiple revenue streams User-friendly trading experience Whether you are a startup planning your first crypto venture or an enterprise targeting global traders, an XT.com clone script offers the flexibility and performance needed to grow in the competitive crypto industry. Businesses looking for a reliable solution can consider the XT.com Clone by Coinexra, which offers feature-rich and scalable crypto exchange development solutions. Read More >> https://www.coinexra.com/xt-com-clone-script
<?xml version ="1.0" encoding="UTF-8"?>
<FlowDefinition xmlns="http://soap.sforce.com/2006/04/metadata">
<activeversionNumber>0</activeversionNumber>
</FlowDefinition>
//Payload in above request for send_broadcast and extras below: $payload = Array( ...//Common 'timezone' => "Asia/Kolkata", //Your timezone (Required) 'datetime' => "2018-06-30 08:30:00", //Schedule Time (YYYY-MM-DD HH:MM:SS)(Required) );
$curl = curl_init();
$payload = Array(
'packageName' => "com.test.yourpackage", //Required Parameter
'type' => "Simple", //Optional Parameter (Default: Simple, Possible Values: Simple, Image)
'title' => "Test Title", //Required Parameter
'description' => "Test Descriptioon", //Optional
'image' => "http://test.test/image.png", //Optional (Works only when type=Image)
'url' => "http://test.com", //Optional (Must be a URL if given)
'ring' => "RING", //Optional (Default: RING, Possible Values: RING, VIBE, RINGVIBE, SILENT)
// Further parameters described below for different APIs
//Above Options are common for all requests.
);
curl_setopt_array($curl, array(
CURLOPT_URL => "https://apis.websitetoapk.com/pushadmin/api/v1/...", // As per your request
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_HTTPHEADER => array(
"content-type: application/x-www-form-urlencoded",
"X-Api-Key: api-key-here",
"X-Auth-Token: token-here"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
$curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => "https://apis.websitetoapk.com/pushadmin/api/v1/list_apps", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_HTTPHEADER => array( "content-type: application/x-www-form-urlencoded", "X-Api-Key: api-key-here", "X-Auth-Token: token-here" ), )); $response = curl_exec($curl); echo $response;
keytool -genkey -keyalg RSA -alias myalias -keystore newkeystore.keystore -storepass yourpassword -validity 36000 -keysize 2048
@media (max-width: 767px) {
.simple-table {
width: 100%;
max-width: 100%;
overflow-x: auto;
overscroll-behavior-inline: contain;
-webkit-overflow-scrolling: touch;
}
.simple-table table {
width: 100%;
min-width: 640px;
}
.simple-table table th,
.simple-table table td {
overflow-wrap: normal;
word-break: normal;
}
}
// ==========================================
// 0. Defining & Configurations
// ==========================================
const DEV_NAMES = ["ItzStubZ"];
function isDev(playerId) {
return DEV_NAMES.includes(api.getEntityName(playerId));
}
// Combined Join Handler for Sidebar Info and Shop Setup
onPlayerJoin = (playerId) => {
// Code 1: Sidebar Setup
api.setClientOption(playerId, "RightInfoText", [
{ str: "Stubbed Survival ", style: { color: "gold", fontWeight: "bold", fontSize: "20px" } },
{ str: "————————————————————————\n", style: { color: "lightgrey", fontWeight: "bold" } },
{ str: "————————————————————————\n", style: { color: "lightgrey", fontWeight: "bold", fontSize: "15px" } },
{ icon: "star", style: { fontSize: "14px", color: "#FFFFA6" } },
{ str: " !help for a list of commands\n Type !rtp to get started!\n", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: "————————————————————————", style: { color: "lightgrey", fontWeight: "bold", fontSize: "15px" } }
]);
// Code 2: Shop Setup
api.createShopItem("classicGame:utilities", "rtp", {
image: "Compass",
customTitle: "Random Teleport",
description: "TP to random place",
buyButtonText: "Teleport"
});
api.configureShopCategory("classicGame:utilities", {
customTitle: "Utilities"
});
};
// ==========================================
// 1. Shop & Commands Handlers (Code 2)
// ==========================================
tick = (ms) => {};
onPlayerBoughtShopItem = (playerId, categoryKey, itemKey, item, userInput) => {
if (itemKey === "rtp") {
const randomX = Math.floor(Math.random() * 200000) - 100000;
const randomY = 100;
const randomZ = Math.floor(Math.random() * 200000) - 100000;
api.setPosition(playerId, [randomX, randomY, randomZ]);
// Fixed template literals to correctly display the coordinates
api.sendMessage(playerId, "Téléporté à [" + randomX + ", " + randomY + ", " + randomZ + "]", { color: "cyan" });
api.broadcastMessage(api.getEntityName(playerId) + " used rtp!", { color: "yellow" });
}
};
playerCommand = (playerId, command) => {
if (command === "shop") {
api.openShop(playerId);
return true;
}
return false;
};
// ==========================================
// 2. Chat Command Handler (Code 1)
// ==========================================
function onPlayerChat(id, msg) {
const trimmedMsg = msg.trim();
const lowerMsg = trimmedMsg.toLowerCase();
// !rtp Chat Command
if (lowerMsg === "!rtp") {
const x = Math.floor(200000 * Math.random() - 100000);
const z = Math.floor(200000 * Math.random() - 100000);
const y = 125;
api.setPosition(id, [x, y, z]);
api.sendMessage(id, "Succesfully Teleported", {color: "lime"});
return false;
}
// !help Menu
if (lowerMsg === "!help") {
const helpMessage = [
{ str: "Stubbed Survival Commands\n", style: { color: "#5CFF5C", fontWeight: "bold" } },
{ str: "!rtp", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Random teleport\n", style: { color: "white" } },
{ str: "!bal", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Check your MelonBucks\n", style: { color: "white" } },
{ str: "!msg <player> <msg>", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Privately message a player\n", style: { color: "white" } },
{ str: "!r.p <character> <text>", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Broadcast a roleplay message\n", style: { color: "white" } },
{ str: "!help", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Shows this menu", style: { color: "white" } }
];
if (isDev(id)) {
helpMessage.push(
{ str: "\n!dev help", style: { color: "#FF5555", fontWeight: "bold" } },
{ str: " - Developer commands", style: { color: "white" } }
);
}
api.sendMessage(id, helpMessage);
return false;
}
// !r.p <character> <text>
if (lowerMsg.startsWith("!r.p ")) {
const args = trimmedMsg.split(" ");
if (args.length < 3) {
api.sendMessage(id, "Usage: !r.p <character> <text>", { color: "red" });
return false;
}
const characterName = args[1];
const roleplayText = args.slice(2).join(" ");
api.broadcastMessage([
{ str: "[Roleplay] ", style: { color: "#00FFCC", fontWeight: "bold" } },
{ str: characterName + ": ", style: { color: "#FFD700", fontWeight: "bold" } },
{ str: roleplayText, style: { color: "#FFFFFF" } }
]);
return false;
}
// !msg <player> <msg>
if (lowerMsg.startsWith("!msg ")) {
const args = trimmedMsg.split(" ");
if (args.length < 3) {
api.sendMessage(id, "Usage: !msg <player> <msg>", { color: "red" });
return false;
}
const targetName = args[1];
const dmMessage = args.slice(2).join(" ");
const targetId = api.getPlayerId(targetName);
if (targetId === null) {
api.sendMessage(id, "Player not found: " + targetName, { color: "red" });
return false;
}
const senderName = api.getEntityName(id);
api.sendMessage(targetId, [
{ str: "[Msg] ", style: { color: "#FF77FF", fontWeight: "bold" } },
{ str: senderName + ": ", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: dmMessage, style: { color: "white" } }
]);
api.sendMessage(id, [
{ str: "[Msg → " + targetName + "] ", style: { color: "#FF77FF", fontWeight: "bold" } },
{ str: dmMessage, style: { color: "white" } }
]);
return false;
}
// !dev help
if (lowerMsg === "!dev help") {
if (!isDev(id)) {
api.sendMessage(id, "Unknown command. Type !help.", { color: "red" });
return false;
}
api.sendMessage(id, [
{ str: "Developer Commands\n", style: { color: "#FF5555", fontWeight: "bold" } },
{ str: "!message <normal|bold> <colour> <msg>", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Broadcast a server message\n", style: { color: "white" } },
{ str: "!kill <player>", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: " - Kill a player", style: { color: "white" } }
]);
return false;
}
// !message <normal|bold> <colour> <msg>
if (lowerMsg.startsWith("!message ")) {
if (!isDev(id)) {
api.sendMessage(id, "Unknown command. Type !help.", { color: "red" });
return false;
}
const args = trimmedMsg.split(" ");
if (args.length < 4) {
api.sendMessage(id, "Usage: !message <normal|bold> <colour> <msg>", { color: "red" });
return false;
}
const weight = args[1].toLowerCase();
const colour = args[2];
const message = args.slice(3).join(" ");
if (weight !== "normal" && weight !== "bold") {
api.sendMessage(id, "First option must be normal or bold.", { color: "red" });
return false;
}
api.broadcastMessage(message, {
color: colour,
fontWeight: weight
});
return false;
}
// !kill <player>
if (lowerMsg.startsWith("!kill ")) {
if (!isDev(id)) {
api.sendMessage(id, "Unknown command. Type !help.", { color: "red" });
return false;
}
const targetName = trimmedMsg.slice(6).trim();
const targetId = api.getPlayerId(targetName);
if (!targetName) {
api.sendMessage(id, "Usage: !kill <player>", { color: "red" });
return false;
}
if (targetId === null) {
api.sendMessage(id, "Player not found: " + targetName, { color: "red" });
return false;
}
api.setHealth(targetId, 0);
api.sendMessage(id, [
{ str: "Killed ", style: { color: "#FF5555" } },
{ str: targetName, style: { color: "white", fontWeight: "bold" } },
{ str: ".", style: { color: "#FF5555" } }
]);
return false;
}
}
// ==========================================
// 3. Totem Logic & Game Actions
// ==========================================
onAttemptKillPlayer = (v) => {
if (api.getEffects(v).includes("Totem")) {
let pos = api.getPosition(v);
api.applyEffect(v, "Health Regen", 3e4, {inbuiltLevel: 2});
api.applyEffect(v, "Heat Resistance", 1e4, {inbuiltLevel: 1});
api.applyEffect(v, "Damage Reduction", 3e4, {inbuiltLevel: 2});
api.applyEffect(v, "Damage", 1e4, {inbuiltLevel: 2});
api.applyEffect(v, "Speed", 1e4, {inbuiltLevel: 2});
api.setHealth(v, 30);
api.setShieldAmount(v, 50);
if (api.getHeldItem(v)?.attributes.customDisplayName === "Totem Of Undying") {
let s = api.getSelectedInventorySlot(v);
api.setItemSlot(v, s, "Air");
api.removeEffect(v, "Totem");
return "preventDeath";
} else {
api.removeEffect(v, "Totem");
return "preventDeath";
}
}
};
// Cleaned up the unfinished handler from Code 1 to prevent syntax errors
onPlayerAttemptAltAction = (id) => {
if (api.getHeldItem(id)?.name == "Gold Spade" && api.getHeldItem(id)?.attributes.customDisplayName == "Totem") {
// Your alternative action logic here
}
};
let
Source = SharePoint.Files("https://bbbind0-my.sharepoint.com/personal/bdusenberry_bbbind_com/", [ApiVersion = 15]),
#"Filtered Rows1" = Table.SelectRows(Source, each Text.Contains([Folder Path], "Desktop")),
#"Filtered Rows" = Table.SelectRows(#"Filtered Rows1", each Text.Contains([Name], "Finished Goods"))
in
#"Filtered Rows"
// !dm <player> <msg>
if (lowerMsg.startsWith("!dm ")) {
const args = trimmedMsg.split(" ")
if (args.length < 3) {
api.sendMessage(id, "Usage: !dm <player> <msg>", { color: "red" })
return false
}
const targetName = args[1]
const dmMessage = args.slice(2).join(" ")
const targetId = api.getPlayerId(targetName)
if (targetId === null) {
api.sendMessage(id, "Player not found: " + targetName, { color: "red" })
return false
}
const senderName = api.getEntityName(id)
api.sendMessage(targetId, [
{ str: "[DM] ", style: { color: "#FF77FF", fontWeight: "bold" } },
{ str: senderName + ": ", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: dmMessage, style: { color: "white" } }
])
api.sendMessage(id, [
{ str: "[DM → " + targetName + "] ", style: { color: "#FF77FF", fontWeight: "bold" } },
{ str: dmMessage, style: { color: "white" } }
])
return false
}
let playerKillsByDbId = {};
/* Configuration: Change this number to give more or less Gold per kill */ //
const GOLD_PER_KILL = 5;
/* Helper function to determine rank and text color based on kills */
function getPlayerRankInfo(kills) {
if (kills >=250) return {name: "REAPER", color: "Black" };
if (kills >= 100) return { name: "MASTER", color: "White" };
if (kills >= 50) return { name: "LEGEND", color: "Blue" };
if (kills >= 25) return { name: "KILLER", color: "Red" };
if (kills >= 10) return { name: "WARRIOR", color: "Orange" };
return { name: "ROOKIE", color: "LightGray" };
}
/* Updates the visual subtitle under the player's name tag */
function updateNameTag(pId) {
const dbId = api.getPlayerDbId(pId);
if (!dbId) return;
const kills = playerKillsByDbId[dbId] || 0;
const rankInfo = getPlayerRankInfo(kills);
api.setTargetedPlayerSettingForEveryone(pId, 'nameTagInfo', {
subtitle: [
{ str: `[${rankInfo.name}] `, style: { color: rankInfo.color } },
{ str: `Kills: ${kills}`, style: { color: 'White' } }
],
subtitleBackgroundColor: 'Black'
}, true);
}
/* Event: Runs when a player joins and fetches permanent saved data */
onPlayerJoin = (playerId) => {
const dbId = api.getPlayerDbId(playerId);
if (!dbId) return;
// Use the temporary playerId to request data from the game's storage server
let savedData = api.getMoonstoneChestItemSlot(playerId, 99);
if (savedData && savedData.count) {
// Map the permanent dbId directly to the loaded count so it survives a tab clear
playerKillsByDbId[dbId] = savedData.count;
} else {
// Only reset to 0 if they have absolutely no save history recorded
if (playerKillsByDbId[dbId] === undefined) {
playerKillsByDbId[dbId] = 0;
}
}
updateNameTag(playerId);
};
/* Event: Runs when a player secures a kill, updates database saves, and awards Gold */
onPlayerKilledOtherPlayer = (killerId, deadId, damage, item) => {
if (killerId != null && killerId !== deadId) {
const killerDbId = api.getPlayerDbId(killerId);
if (!killerDbId) return;
// 1. Permanently update the score counter assigned to their real account profile
playerKillsByDbId[killerDbId] = (playerKillsByDbId[killerDbId] || 0) + 1;
// 2. Instruct the game core to lock that data into an invisible container
api.setMoonstoneChestItemSlot(killerId, 99, "Diamond", playerKillsByDbId[killerDbId]);
updateNameTag(killerId);
// 3. Award Gold Bars directly to inventory
api.giveItem(killerId, "Gold Bar", GOLD_PER_KILL);
// 4. FIXED: Replaced non-existent notification function with official messaging function
api.sendMessage(killerId, `+${GOLD_PER_KILL} Gold Bars for the kill! Total Kills: ${playerKillsByDbId[killerDbId]}`, { color: "gold" });
}
};
/* Global variables to manage the weapon systems */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
if (typeof magmaWandTimers === "undefined") {
var magmaWandTimers = {};
}
/* Global dictionary to manage weapon timestamps */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
/* Custom Weapon Abilities & Right-Click Magma Wand Functionality */
/* Triggered whenever a player right-clicks or interacts while holding an item */
onPlayerAltAction = (playerId) => {
try {
const heldItem = api.getHeldItem(playerId);
if (!heldItem) return;
/* Checks for a Molten Magma Rod specifically named Magma Wand */
if (
heldItem.name === "Molten Magma Rod" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Magma Wand"
) {
const currentTime = Date.now();
const lastUsed = magmaWandCooldowns[playerId] || 0;
const cooldownDuration = 2500; /* 2.5 seconds */
/* Blocks firing if 2.5 seconds have not passed */
if (currentTime - lastUsed < cooldownDuration) {
return;
}
const playerPos = api.getPosition(playerId);
if (playerPos && Array.isArray(playerPos)) {
const px = playerPos[0];
const py = playerPos[1];
const pz = playerPos[2];
/* Get looking direction to fire the fireballs forward */
let facing = [0, 0, -1];
const facingInfo = api.getPlayerFacingInfo ? api.getPlayerFacingInfo(playerId) : null;
if (facingInfo && facingInfo.dir) {
facing = facingInfo.dir;
}
const fx = facing[0];
const fy = facing[1];
const fz = facing[2];
/* Set the cooldown timestamp immediately before firing */
magmaWandCooldowns[playerId] = currentTime;
/* LAUNCH 9 FIREBALLS IN A COMPACT, SMALLER 3x3 SQUARE GRID PATTERN */
/* MIDDLE ROW (Eye Level - Small 0.08 spacing) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy, fz - 0.08], 2, 2, 0);
/* TOP ROW (Tighter vertical offset) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy + 0.08, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy + 0.08, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy + 0.08, fz - 0.08], 2, 2, 0);
/* BOTTOM ROW (Tighter vertical offset) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy - 0.08, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy - 0.08, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy - 0.08, fz - 0.08], 2, 2, 0);
}
}
} catch (err) {
api.log("Error in Magma Wand Right-Click: " + err);
}
};
/* Global dictionary to manage weapon timestamps */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
/* Custom Weapon Abilities & Right-Click Magma Wand Functionality */
/* Triggered whenever a player right-clicks or interacts while holding an item */
onPlayerAltAction = (playerId) => {
try {
const heldItem = api.getHeldItem(playerId);
if (!heldItem) return;
/* Checks for a Molten Magma Rod specifically named Magma Wand */
if (
heldItem.name === "Molten Magma Rod" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Magma Wand"
) {
const currentTime = Date.now();
const lastUsed = magmaWandCooldowns[playerId] || 0;
const cooldownDuration = 2500; /* 2.5 seconds in milliseconds */
/* FIXED: Blocks firing instantly if 2.5 seconds have not passed */
if (currentTime - lastUsed < cooldownDuration) {
return;
}
const playerPos = api.getPosition(playerId);
if (playerPos && Array.isArray(playerPos)) {
const px = playerPos[0];
const py = playerPos[1];
const pz = playerPos[2];
/* Get looking direction to fire the fireballs forward */
let facing = [0, 0, -1];
const facingInfo = api.getPlayerFacingInfo ? api.getPlayerFacingInfo(playerId) : null;
if (facingInfo && facingInfo.dir) {
facing = facingInfo.dir;
}
const fx = facing[0];
const fy = facing[1];
const fz = facing[2];
/* Set the cooldown timestamp immediately before firing */
magmaWandCooldowns[playerId] = currentTime;
/* LAUNCH 9 FIREBALLS IN A 3x3 SQUARE GRID PATTERN */
/* MIDDLE ROW (Eye Level) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy, fz - 0.2], 2, 2, 0);
/* TOP ROW (Angled Upward) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy + 0.2, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy + 0.2, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy + 0.2, fz - 0.2], 2, 2, 0);
/* BOTTOM ROW (Angled Downward) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy - 0.2, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy - 0.2, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy - 0.2, fz - 0.2], 2, 2, 0);
}
}
} catch (err) {
api.log("Error in Magma Wand Right-Click: " + err);
}
};
/* Triggered when damaging players with standard weapons */
onPlayerDamagingOtherPlayer = (attackerId, targetId, damageAmount, itemName, bodyPartHit, damagerDbId) => {
try {
const heldItem = api.getHeldItem(attackerId);
if (!heldItem) return;
/* Poison knife */
if (
heldItem.name === "Iron Dagger" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Poison Knife"
) {
api.applyEffect(targetId, "Weakness", 5000, { displayName: "Weakness", icon: "Weakness" });
api.applyEffect(targetId, "Poisoned", 2500, { displayName: "Poisoned", icon: "Poisoned" });
}
/* Wind Mace */
if (
heldItem.name === "Moonstone Mace" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Wind Mace"
) {
const attackerPos = api.getPosition(attackerId);
if (!attackerPos || !Array.isArray(attackerPos)) return;
const x = attackerPos[0];
const y = attackerPos[1];
const z = attackerPos[2];
if (y - Math.floor(y) > 0.1) {
api.applyHealthChange(targetId, -20, attackerId);
api.applyEffect(targetId, "Slowness", 5000, {
icon: "Slowness",
displayName: "Slammed",
inbuiltLevel: 2
});
api.setVelocity(attackerId, 0, 20, 0);
/* FIXED: Fully restored all array values to prevent unexpected comma crashes */
api.playParticleEffect({
dir1: [-3, -3, -3],
dir2: [3, 3, 3],
pos1: [x - 3, y, z - 3],
pos2: [x + 3, y + 3, z + 3],
texture: "glint",
minLifeTime: 0.3,
maxLifeTime: 1,
minEmitPower: 3,
maxEmitPower: 5,
minSize: 0.3,
maxSize: 0.7,
manualEmitCount: 100,
gravity: [0, -5, 0],
colorGradients: [
{
timeFraction: 0,
minColor: [200, 200, 255],
maxColor: [255, 255, 255]
}
],
velocityGradients: [
{
timeFraction: 0,
factor: 1,
factor2: 1
}
],
blendMode: 1
});
return "preventDamage";
}
}
} catch (err) {
api.log("Error in Weapon Script: " + err);
}
};
Ready-made binary options trading platforms offer faster deployment, but they often limit customization, branding, and future scalability. As businesses grow, these limitations can affect user experience and platform flexibility. Custom binary options trading platform development gives businesses full control over features, security, and integrations. For companies looking to build a scalable and competitive trading platform, Softean is a trusted binary options trading platform development company delivering tailored solutions for long-term growth.
.wcgs_xzoom,
.wcgs_xzoom-source,
.wcgs_xzoom-source *,
.wcgs_xzoom-lens,
.wcgs_xzoom-container,
.wcgs_xzoom-hidden {
background: transparent !important;
background-color: transparent !important;
}
.wcgs_xzoom-preview {
background: transparent !important;
}
RWA Tokenization is the process of converting real-world assets such as real estate, gold, commodities, and financial assets into blockchain-based digital tokens. It enables fractional ownership, transparent transactions, improved liquidity, and secure asset management through blockchain technology.
//|--------------|//
//| DO NOT TOUCH |//
//|______________|//
let i = 0;let light = 1; let ropes = 0;onPlayerChat = (pid, msg, chat) => {const allowed = ["ItzStubZ"];if (msg.startsWith('!clear ') && allowed.includes(api.getEntityName(pid))) { const u = msg.split(" ");api.broadcastMessage(`Cleared ${u[1]}'s inventory!`, { color: "gold" });api.clearInventory(api.getPlayerId(u[1]));}if (msg.startsWith('!bringall') && allowed.includes(api.getEntityName(pid))) { const here = api.getPosition(pid);let moved = 0;for (const id of api.getPlayerIds()) {if (id !== pid) {api.setPosition(id, here);moved++;}}
api.broadcastMessage(`Brought ${moved} players to ${api.getEntityName(pid)}!`, { color: "gold" });}if (msg.startsWith('!tp ') && allowed.includes(api.getEntityName(pid))) {const u = msg.split(" ");api.broadcastMessage(`Teleported to ${u[1]}!`, { color: "gold" });api.setPosition(pid, api.getPosition(api.getPlayerId(u[1])));}if (msg.startsWith('!bring ') && allowed.includes(api.getEntityName(pid))) {const u = msg.split(" ");api.broadcastMessage(`Brought ${u[1]}!`, { color: "gold" });api.setPosition(api.getPlayerId(u[1]), api.getPosition(pid));}if (chat !== "Tribe") {
const name = api.getEntityName(pid);
if (name === "HerobrineRobloxTV") {
api.broadcastMessage([
{ str: "[", style: { color: "black" } },
{ icon: "crown fa-bounce", style: { color: "gold" } },
{ str: " 👑OWNER👑", style: { color: "cyan" } },
{ str: "] ", style: { color: "black" } },
{ str: name, style: { color: "blue" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (name === "ItzStubZ") {
api.broadcastMessage([
{ str: "[", style: { color: "black" } },
{ icon: "crown fa-bounce", style: { color: "#F4320B" } },
{ str: " Admin ", style: { color: "red" } },
{ str: "] ", style: { color: "black" } },
{ str: name, style: { color: "#F4320B" } },
{ str: ": ", style: { color: "#F4320B" } },
{ str: msg, style: { color: "white" } },
]);
} else if (name === "Gamermixer_Adam_0w0") {
api.broadcastMessage([
{ str: "[", style: { color: "black" } },
{ icon: "fa-shield fa-bounce", style: { color: "white" } },
{ str: " Mod", style: { color: "black" } },
{ str: "] ", style: { color: "black" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (api.getItemSlot(pid, 46)?.name === "HerobrineRobloxTV") {
api.broadcastMessage([
{ str: "[", style: { color: "#db9a32" } },
{ icon: "star", style: { color: "white" } },
{ str: " Hero", style: { color: "#94c2ce" } },
{ str: "] ", style: { color: "#db9a32" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (api.getItemSlot(pid, 46)?.name === "Cyan Wood Helmet") {
api.broadcastMessage([
{ str: "[", style: { color: "#4479ff" } },
{ icon: "star", style: { color: "white" } },
{ str: " Support Student", style: { color: "#35a4ff" } },
{ str: "] ", style: { color: "#4479ff" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (api.getItemSlot(pid, 46)?.name === "Diamond Helmet") {
api.broadcastMessage([
{ str: "[", style: { color: "black" } },
{ icon: "star", style: { color: "gray" } },
{ str: " Businuss Student", style: { color: "cyan" } },
{ str: "] ", style: { color: "cyan" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (api.getItemSlot(pid, 46)?.name === "Purple Wood Helmet") {
api.broadcastMessage([
{ str: "[", style: { color: "blue" } },
{ icon: "star", style: { color: "white" } },
{ str: " Nurse", style: { color: "#7763e8" } },
{ str: "] ", style: { color: "blue" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else if (api.getItemSlot(pid, 46)?.name === "Lime Wood Helmet") {
api.broadcastMessage([
{ str: "[", style: { color: "white" } },
{ icon: "star", style: { color: "red" } },
{ str: " Aid", style: { color: "#red" } },
{ str: "] ", style: { color: "white" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
} else {
api.broadcastMessage([
{ str: "[", style: { color: "Gray" } },
{ icon: "user", style: { color: "Gray" } },
{ str: " No role yet", style: { color: "Gray" } },
{ str: "] ", style: { color: "Gray" } },
{ str: name, style: { color: "white" } },
{ str: ": ", style: { color: "white" } },
{ str: msg, style: { color: "white" } },
]);
}
return false;
}
};
onPlayerJoin = (pid) => {
if (api.getItemSlot(pid, 47)?.name === undefined || api.getItemSlot(pid, 47)?.name === 'LWhite Wood Chestplate') {
api.setItemSlot(pid, 47, "Light Gray Wood Chestplate", 1, {
customDisplayName: `Uniform shirt`,
customDescription: `Yo school uniform shirt.`,
});
api.setItemSlot(pid, 49, "Black Wood Leggings", 1, {
customDisplayName: "Uniform Pants",
customDescription: "Your school uniform pants.",
});
api.setItemSlot(pid, 50, "Black Wood Boots", 1, {
customDisplayName: "Uniform Shoes",
customDescription: "Your school uniform shoes.",
});
}
const nameTagInfo = {
subtitle: [{ str: `student` }],
};
api.setTargetedPlayerSettingForEveryone(pid, "nameTagInfo", nameTagInfo);
const name = api.getEntityName(pid);
if (name === "Gapviz") {
api.broadcastMessage([
{ str: "The Owner ", style: { color: "#679ec1" } },
{ str: name, style: { color: "white" } },
{ str: " has joined the game", style: { color: "#679ec1" } },
]);
} else if (name === "bloxdio_nation") {
api.broadcastMessage([
{ str: "The Admin ", style: { color: "#ffc83d" } },
{ str: name, style: { color: "white" } },
{ str: " has joined the game", style: { color: "#ffc83d" } },
]);
} else if (name === "Nightmare_MC") {
api.broadcastMessage([
{ str: "The Mod ", style: { color: "orange" } },
{ str: name, style: { color: "white" } },
{ str: " has joined the game", style: { color: "orange" } },
]);
} else if (name === "DEKU_HERO12") {
api.broadcastMessage([
{ str: "The co-owner ", style: { color: "green" } },
{ str: name, style: { color: "white" } },
{ str: " has joined the game", style: { color: "green" } },
]);
} else if (name === "Omz_fun_gurl_power294") {
api.broadcastMessage([
{ str: "The co-owner ", style: { color: "pink" } },
{ str: name, style: { color: "white" } },
{ str: " has joined the game", style: { color: "pink" } },
]);
} else {
api.broadcastMessage([
{ str: name, style: { color: "white" } },
{ str: " has joined the game! Say hi!", style: { color: "#3273ff" } },
]);
}
api.setWalkThroughRect(pid, [-10000, -10000, -10000], [-10000, 10000, 10000], 0);
};
onPlayerLeave = (pid) => {
const name = api.getEntityName(pid);
api.broadcastMessage([
{ str: name, style: { color: "white" } },
{ str: " has left the game", style: { color: "red" } },
]);
};
function sidebarUI(playerId){
const space = "----------------------------\n"
api.setClientOption(playerId, "RightInfoText", [
{ str: "Hero Survival Server\n", style: { color: "red", fontSize: "25px", fontWeight: "800" } },
{ str: space, style: { color: "#555" } },
{ icon: "crown", style: { color: "gold" } },
{ str: " OWNER:\n", style: { color: "gold", fontWeight: "700" } },
{ str: "HerobrineRobloxTV\n", style: { color: "red" } },
{ str: space, style: { color: "#555" } },
{ icon: "crown", style: { color: "magenta" } },
{ str: " Co-owner:\n", style: { color: "red", fontWeight: "700" } },
{ str: "ItzStubZ\n", style: { color: "magenta", fontWeight: "700" } },
{ str: space, style: { color: "#555" } },
{ icon: "wrench", style: { color: "blue" } },
{ str: " CUSTOM SMP\n", style: { color: "red", fontWeight: "700" } },
{ str: " - Welcome to Hero Smp!\n", style: { color: "cyan" } },
{ str: " - Earn money! Get rich! And cause mass destruction on other players!\n", style: { color: "cyan" } },
{ str: space, style: { color: "#555" } },
{ str: "BEST SMP fr\n", style: { color: "RED" } },
])
}
onPlayerJoin=(playerId) => {
sidebarUI(playerId)
}
A white label tokenization platform typically offers a better ROI for businesses that want to launch quickly and keep development costs under control. Instead of spending months building blockchain infrastructure, smart contracts, and compliance features from scratch, businesses can deploy a ready-made solution and focus on growth. Custom development suits highly specialized projects, but for most use cases, a white label solution is the smarter investment. Coinexra's white label tokenization platform provides enterprise-grade features, security, customization, and scalability, making it an excellent choice for launching a tokenization business faster.
function debounce(callback, delay = 300) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
// Example: avoid running a search request on every keystroke
const search = debounce((query) => {
console.log("Searching for:", query);
}, 500);
search("javascript");
search("javascript debounce");
search("javascript debounce function");
##healthcare ##menshealth #erectiledysfunction
Fildena Super Active For Treat Erectile Dysfunction (ED) In MaleFildena Super Active is a medicine marketed for the treatment of erectile dysfunction (ED), a condition that can make it difficult to achieve or maintain an erection suitable for sexual activity. It contains sildenafil, a PDE5 inhibitor that may improve blood flow to the penis when sexual stimulation is present. Treatment should be used only after discussing symptoms, medical history, and other medicines with a qualified healthcare professional. Sildenafil may not be suitable for everyone, particularly people taking nitrate medicines or certain cardiovascular treatments. Possible side effects can include headache, flushing, indigestion, nasal congestion, or dizziness. Seek medical advice if side effects are severe or persistent.
.marquee {
display: inline-flex;
animation: scroll 500s ease infinite;
flex-direction: row;
flex: 0 1 auto;
white-space: nowrap;
gap: 40px;
font-family: var(--e-global-typography-ec9ea48-font-family), Sans-serif;
font-size: var(--e-global-typography-ec9ea48-font-size);
font-weight: var(--e-global-typography-ec9ea48-font-weight);
line-height: calc(var(--e-global-typography-ec9ea48-line-height) + 20px);
letter-spacing: var(--e-global-typography-ec9ea48-letter-spacing);
color: var(--e-global-color-primary);
}
.maskbg{
mask-image: linear-gradient(to right, transparent 0%, black 15%, black 85%, transparent 100%);
overflow: hidden;
}
.marquee:hover {
animation-play-state: paused;
}
@keyframes scroll {
0% { transform: translateX(0%); }
25% { transform: translateX(-25%); }
50% { transform: translateX(-50%); }
75% { transform: translateX(-75%); }
100% { transform: translateX(-100%); }
}
.faqsa {
}
api.giveItem(myId, "Gold Coin", 10, { customDisplayName: "Yen", customAttributes: { enchantments: {}, enchantmentTier: "Tier 5" } });
/* Bloxd.io Shop Script - Fixed removeItemName Argument Error */
let price = 1; /* Cost in Yen */
let amountToBuy = 1; /* Amount of Dirt given */
/* Look at what the player is currently holding in their hand */
let heldItem = api.getHeldItem(myId);
/* Validate: Holding a Gold Coin AND its name is exactly 'Yen' */
if (heldItem && heldItem.name === "Gold Coin" && heldItem.attributes && heldItem.attributes.customDisplayName === "Yen") {
/* Check if they are holding enough of it */
if (heldItem.amount >= price) {
/* Fixed: Exactly 3 arguments passed to removeItemName */
api.removeItemName(myId, "Gold Coin", price);
/* Attempt to give the player their Dirt */
api.giveItem(myId, "Dirt", amountToBuy);
/* Inventory full safety check */
if (api.inventoryIsFull(myId)) {
/* Refund the exact item layout if their inventory had no space */
api.giveItem(myId, "Gold Coin", price, { customDisplayName: "Yen" });
api.sendMessage(myId, "Your inventory is full! Yen refunded.", { color: "red" });
} else {
api.sendMessage(myId, "Successfully bought " + amountToBuy + " Dirt!", { color: "green" });
}
} else {
api.sendMessage(myId, "You need to hold at least " + price + " Yen in your hand!", { color: "red" });
}
} else {
/* Triggers if they hold normal Gold Coins or an unrelated block */
api.sendMessage(myId, "Please hold your 'Yen' coins in your hand to buy!", { color: "yellow" });
}
/* 1. Grab the looking direction vector array: [X, Y, Z] */ let facing = api.getPlayerFacingInfo(playerId).dir; /* 2. Multiply the angles by your desired launch speed (Multiplier) */ let speedMultiplier = 40; let launchX = facing[0] * speedMultiplier; let launchY = facing[1] * speedMultiplier; let launchZ = facing[2] * speedMultiplier; /* 3. Apply the 4-argument impulse payload natively to the clicking player */ api.applyImpulse(playerId, launchX, launchY, launchZ);
// start music
const MUSIC_LOOP_TIME = 112000;
const musicStartTimes = {};
function startEpicMusic(playerId) {
api.setClientOption(playerId, "music", null);
api.setClientOption(playerId, "music", "Emotional Epic");
api.setClientOption(playerId, "musicVolumeLevel", 0.6);
musicStartTimes[playerId] = Date.now();
}
/* 1. Global callback that triggers whenever a player joins */
function onPlayerJoin(playerId) {
// MUSIC
function onPlayerJoin(playerId) {
startEpicMusic(playerId);
api.setClientOption(playerId, "RightInfoText", [
{ str: "🍉 Watermelon ", style: { color: "#F01E0F", fontWeight: "bold", fontSize: "20px" } },
{ str: "SMP 💦\n", style: { color: "#5CFF5C", fontWeight: "bold", fontSize: "20px" } },
/* rest of sidebar */
]);
/* join messages etc */
}
/* 2. Set up the Right Sidebar Info Text */
api.setClientOption(playerId, "RightInfoText", [
{ str: "🍉 Watermelon ", style: { color: "#F01E0F", fontWeight: "bold", fontSize: "20px" } },
{ str: "SMP 💦\n", style: { color: "#5CFF5C", fontWeight: "bold", fontSize: "20px" } },
{ str: "————————————————————————\n", style: { color: "lightgrey", fontWeight: "bold" } },
/* Discord Row */
{ icon: "discord", style: { fontSize: "14px", color: "#5865F2" } },
{ str: " discord.gg/b3sHbhwum7\n", style: { color: "#5865F2", fontWeight: "bold" } },
/* YouTube Row */
{ icon: "youtube", style: { fontSize: "14px", color: "#FF0000" } },
{ str: " @z0x_-bloxd\n", style: { color: "#FF0000", fontWeight: "bold" } },
/* Extras Row*/
{ str: "————————————————————————\n", style: { color: "lightgrey", fontWeight: "bold", fontSize: "15px" } },
{ icon: "star", style: { fontSize: "14px", color: "#FFFFA6" } },
{ str: " Make sure to check the Info warp!\n Type !rtp to get started!\n", style: { color: "#FFFFA6", fontWeight: "bold" } },
{ str: "————————————————————————", style: { color: "lightgrey", fontWeight: "bold", fontSize: "15px" } },
]);
/* 3. Send the chat messages instantly */
api.sendMessage(playerId, "—— Welcome to 🍉 Watermelon-SMP 💦! ——", { color: "red", fontWeight: "bold" });
api.sendMessage(playerId, "📝 Latest Updates 📝:", { color: "gold" });
api.sendMessage(playerId, "— New GUI for sidebar and Suggestion Box at spawn.\n— Forge has been RELEASED! (extras at ItzStubZ's Plot)\n— MOON IS OUT SORTA, still not finished tho.\n", { color: "white" });
api.sendMessage(playerId, "—————————————————", { color: "red" });
}
function setMusic(playerId) {
api.setClientOption(
playerId,
"music",
"Epic1"
);
api.setClientOption(
playerId,
"musicVolumeLevel",
0.6
);
}
/* ================
RANKS SYSTEM & RTP COMMAND
================ */
function onPlayerChat(id, msg) {
// 1. RTP COMMAND CHECK (Checks if any player typed !rtp)
if (msg.trim() === "!rtp") {
// Generates random coordinates between -100,000 and 100,000
const x = Math.floor(200000 * Math.random() - 100000);
const z = Math.floor(200000 * Math.random() - 100000);
const y = 125; // Safe standard height above ground level
// Teleports the player
api.setPosition(id, [x, y, z]);
// Sends a private confirmation message to that specific player
// Stops the function here so '!rtp' doesn't show up in global chat
return false;
}
// 2. EXISTING RANK SYSTEM
let name = api.getEntityName(id);
// Windows7601
if (name === "Windows7601") {
api.broadcastMessage([
{ str: "[", style: { color: "white" } }, { icon: "question-mark", style: { color: "lightgrey" } }, { str: "IDK", style: { color: "lightgrey" } }, { str: "] ", style: { color: "white" } },
{ str: "Windows7601: ", style: { color: "white" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// ItzStubZ brother
if (name === "Tiny_asian_Squirtle") {
api.broadcastMessage([
/*discord*/
{ icon: "discord", style: { color: "#5865F2" } }, { str: " ", style: { color: "white" } },
{ str: "[", style: { color: "white" } }, { icon: "cog", style: { color: "lightgrey" } }, { str: "Clan", style: { color: "lightgrey" } }, { str: "ker Jr.", style: { color: "lightgrey" } }, { str: "] ", style: { color: "white" } },
{ str: "Tiny_asian_Squirtle: ", style: { color: "white" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// ItzStubZ
if (name === "ItzStubZ") {
api.broadcastMessage([
/*discord*/
{ icon: "discord", style: { color: "#5865F2" } }, { str: " ", style: { color: "white" } },
{ str: "[", style: { color: "white" } }, { icon: "cog", style: { color: "lightgrey" } }, { str: "Clan", style: { color: "lightgrey" } }, { str: "ker", style: { color: "lightgrey" } }, { str: "] ", style: { color: "white" } },
{ str: "[", style: { color: "red" } }, { icon: "Watermelon Slice", style: { color: "lightgrey" } }, { str: "Watermelon", style: { color: "lime" } }, { str: "]", style: { color: "red" } },
{ str: "[", style: { color: "cyan" } }, { icon: "cog", style: { color: "cyan" } }, { str: " Programmer] ", style: { color: "cyan" } },
{ str: "ItzStubZ: ", style: { color: "gold" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// z0x_
if (name === "z0x_") {
api.broadcastMessage([
/*discord*/
{ icon: "discord", style: { color: "red" } }, { str: " ", style: { color: "white" } },
{ str: " [", style: { color: "red" } }, { icon: "Damage", style: { color: "red" } }, { str: "PvPer", style: { color: "red" } }, { str: "] ", style: { color: "red" } },
{ str: "𖤓 z0x_: ", style: { color: "red" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// Cyber_NebulaX
if (name === "Cyber_NebulaX") {
api.broadcastMessage([
{ str: "[", style: { color: "brown" } }, { icon: "Mob Slayer", style: { color: "red" } }, { str: "Survivor", style: { color: "red" } }, { str: "] ", style: { color: "brown" } },
{ str: "[", style: { color: "purple" } }, { icon: "star", style: { color: "purple" } }, { str: "Nebula", style: { color: "purple" } }, { str: "] ", style: { color: "purple" } },
{ str: "[", style: { color: "#3D2D3C" } }, { icon: "shield", style: { color: "#5C3E5A" } }, { str: "M", style: { color: "#73416F" } }, { str: "A", style: { color: "#94478E" } }, { str: "X", style: { colour: "#A845A0" } }, { str: "] ", style: {colour: "#C93CBE" } },
{ str: "Cyber_NebulaX: ", style: { color: "#A746B8" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// Drellie1
if (name === "Drellie1") {
api.broadcastMessage([
{ str: "[", style: { color: "purple" } }, { icon: "star", style: { color: "purple" } }, { str: "Nebula", style: { color: "purple" } }, { str: "] ", style: { color: "purple" } },
{ str: "[", style: { color: "red" } }, { icon: "swords", style: { color: "red" } }, { str: "Killer", style: { color: "red" } }, { str: "] ", style: { color: "red" } },
{ str: "Drellie1: ", style: { color: "orange" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
// ZaLegend
if (name === "ZaLegend") {
api.broadcastMessage([
/*discord*/
{ icon: "discord", style: { color: "#5865F2" } }, { str: " ", style: { color: "white" } },
{ str: "ZaLegend: ", style: { color: "white" } },
{ str: " " + msg, style: { color: "white" } }
]);
return false;
}
}
/* Global variables to manage the weapon systems */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
if (typeof magmaWandTimers === "undefined") {
var magmaWandTimers = {};
}
/* Global dictionary to manage weapon timestamps */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
/* Custom Weapon Abilities & Right-Click Magma Wand Functionality */
/* Triggered whenever a player right-clicks or interacts while holding an item */
onPlayerAltAction = (playerId) => {
try {
const heldItem = api.getHeldItem(playerId);
if (!heldItem) return;
/* Checks for a Molten Magma Rod specifically named Magma Wand */
if (
heldItem.name === "Molten Magma Rod" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Magma Wand"
) {
const currentTime = Date.now();
const lastUsed = magmaWandCooldowns[playerId] || 0;
const cooldownDuration = 2500; /* 2.5 seconds */
/* Blocks firing if 2.5 seconds have not passed */
if (currentTime - lastUsed < cooldownDuration) {
return;
}
const playerPos = api.getPosition(playerId);
if (playerPos && Array.isArray(playerPos)) {
const px = playerPos[0];
const py = playerPos[1];
const pz = playerPos[2];
/* Get looking direction to fire the fireballs forward */
let facing = [0, 0, -1];
const facingInfo = api.getPlayerFacingInfo ? api.getPlayerFacingInfo(playerId) : null;
if (facingInfo && facingInfo.dir) {
facing = facingInfo.dir;
}
const fx = facing[0];
const fy = facing[1];
const fz = facing[2];
/* Set the cooldown timestamp immediately before firing */
magmaWandCooldowns[playerId] = currentTime;
/* LAUNCH 9 FIREBALLS IN A COMPACT, SMALLER 3x3 SQUARE GRID PATTERN */
/* MIDDLE ROW (Eye Level - Small 0.08 spacing) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy, fz - 0.08], 2, 2, 0);
/* TOP ROW (Tighter vertical offset) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy + 0.08, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy + 0.08, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy + 0.08, fz - 0.08], 2, 2, 0);
/* BOTTOM ROW (Tighter vertical offset) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy - 0.08, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.08, fy - 0.08, fz + 0.08], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.08, fy - 0.08, fz - 0.08], 2, 2, 0);
}
}
} catch (err) {
api.log("Error in Magma Wand Right-Click: " + err);
}
};
/* Global dictionary to manage weapon timestamps */
if (typeof magmaWandCooldowns === "undefined") {
var magmaWandCooldowns = {};
}
/* Custom Weapon Abilities & Right-Click Magma Wand Functionality */
/* Triggered whenever a player right-clicks or interacts while holding an item */
onPlayerAltAction = (playerId) => {
try {
const heldItem = api.getHeldItem(playerId);
if (!heldItem) return;
/* Checks for a Molten Magma Rod specifically named Magma Wand */
if (
heldItem.name === "Molten Magma Rod" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Magma Wand"
) {
const currentTime = Date.now();
const lastUsed = magmaWandCooldowns[playerId] || 0;
const cooldownDuration = 2500; /* 2.5 seconds in milliseconds */
/* FIXED: Blocks firing instantly if 2.5 seconds have not passed */
if (currentTime - lastUsed < cooldownDuration) {
return;
}
const playerPos = api.getPosition(playerId);
if (playerPos && Array.isArray(playerPos)) {
const px = playerPos[0];
const py = playerPos[1];
const pz = playerPos[2];
/* Get looking direction to fire the fireballs forward */
let facing = [0, 0, -1];
const facingInfo = api.getPlayerFacingInfo ? api.getPlayerFacingInfo(playerId) : null;
if (facingInfo && facingInfo.dir) {
facing = facingInfo.dir;
}
const fx = facing[0];
const fy = facing[1];
const fz = facing[2];
/* Set the cooldown timestamp immediately before firing */
magmaWandCooldowns[playerId] = currentTime;
/* LAUNCH 9 FIREBALLS IN A 3x3 SQUARE GRID PATTERN */
/* MIDDLE ROW (Eye Level) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy, fz - 0.2], 2, 2, 0);
/* TOP ROW (Angled Upward) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy + 0.2, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy + 0.2, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy + 0.2, fz - 0.2], 2, 2, 0);
/* BOTTOM ROW (Angled Downward) */
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx, fy - 0.2, fz], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx - 0.2, fy - 0.2, fz + 0.2], 2, 2, 0);
api.attemptCreateThrowable(playerId, "Fireball", [px, py + 1.5, pz], [fx + 0.2, fy - 0.2, fz - 0.2], 2, 2, 0);
}
}
} catch (err) {
api.log("Error in Magma Wand Right-Click: " + err);
}
};
/* Triggered when damaging players with standard weapons */
onPlayerDamagingOtherPlayer = (attackerId, targetId, damageAmount, itemName, bodyPartHit, damagerDbId) => {
try {
const heldItem = api.getHeldItem(attackerId);
if (!heldItem) return;
/* Abyssal Sword */
if (
heldItem.name === "Knight Sword" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Abyssal Sword"
) {
api.applyEffect(targetId, "Weakness", 5000, { displayName: "Weakness", icon: "Weakness" });
api.applyEffect(targetId, "Blindness", 5000, { displayName: "Blind", icon: "Blindness" });
}
/* Sun Blade */
if (
heldItem.name === "Gold Sword" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Sun Blade"
) {
api.applyEffect(targetId, "Blindness", 5000, { displayName: "Flashbanged", icon: "Blindness" });
api.applyEffect(targetId, "Poisoned", 5000, { displayName: "Flamed", icon: "Fire Resistance" });
}
/* Poison knife */
if (
heldItem.name === "Iron Dagger" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Poison Knife"
) {
api.applyEffect(targetId, "Weakness", 5000, { displayName: "Weakness", icon: "Weakness" });
api.applyEffect(targetId, "Poisoned", 2500, { displayName: "Poisoned", icon: "Poisoned" });
}
/* frost whip */
if (
heldItem.name === "Diamond Whip" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Frost Whip"
) {
api.applyEffect(targetId, "Frozen", 1000, { displayName: "Frozen", icon: "Frozen" });
api.applyEffect(targetId, "Slowness", 2500, { displayName: "Icey", icon: "Slowness" });
}
/* Wind Mace */
if (
heldItem.name === "Moonstone Mace" &&
heldItem.attributes &&
heldItem.attributes.customDisplayName === "Wind Mace"
) {
const attackerPos = api.getPosition(attackerId);
if (!attackerPos || !Array.isArray(attackerPos)) return;
const x = attackerPos[0];
const y = attackerPos[1];
const z = attackerPos[2];
if (y - Math.floor(y) > 0.1) {
api.applyHealthChange(targetId, -20, attackerId);
api.applyEffect(targetId, "Slowness", 5000, {
icon: "Slowness",
displayName: "Slammed",
inbuiltLevel: 2
});
api.setVelocity(attackerId, 0, 20, 0);
/* FIXED: Fully restored all array values to prevent unexpected comma crashes */
api.playParticleEffect({
dir1: [-3, -3, -3],
dir2: [3, 3, 3],
pos1: [x - 3, y, z - 3],
pos2: [x + 3, y + 3, z + 3],
texture: "glint",
minLifeTime: 0.3,
maxLifeTime: 1,
minEmitPower: 3,
maxEmitPower: 5,
minSize: 0.3,
maxSize: 0.7,
manualEmitCount: 100,
gravity: [0, -5, 0],
colorGradients: [
{
timeFraction: 0,
minColor: [200, 200, 255],
maxColor: [255, 255, 255]
}
],
velocityGradients: [
{
timeFraction: 0,
factor: 1,
factor2: 1
}
],
blendMode: 1
});
return "preventDamage";
}
}
} catch (err) {
api.log("Error in Weapon Script: " + err);
}
};
function tick() {
const now = Date.now();
for (const playerId of api.getPlayerIds()) {
if (!musicStartTimes[playerId]) {
startEpicMusic(playerId);
continue;
}
if (now - musicStartTimes[playerId] >= MUSIC_LOOP_TIME) {
startEpicMusic(playerId);
}
}
}
An optimized and corrected version of the complete Python data processing pipeline is provided below.
from collections import deque, defaultdictimport datetimefrom typing import List, Dict, Tuple, Any
# =====================================================================# 1. PURCHASE WINDOW FILTERING# =====================================================================def get_frequent_users(events: List[Tuple[str, str]], n: int) -> List[str]:
"""
Returns users who made more than N purchases in any 30-day window.
events: List of tuples (user_id, timestamp_str) where timestamp_str is 'YYYY-MM-DD'
"""
user_history = defaultdict(list)
for user_id, t_str in events:
dt = datetime.datetime.strptime(t_str, "%Y-%m-%d")
user_history[user_id].append(dt)
frequent_users = []
for user_id, timestamps in user_history.items():
timestamps.sort() # Linearithmic sort per user for chronological order
left = 0
max_in_window = 0
for right in range(len(timestamps)):
# Maintain sliding window boundaries for exactly 30 days
while (timestamps[right] - timestamps[left]).days > 30:
left += 1
current_window_count = right - left + 1
if current_window_count > max_in_window:
max_in_window = current_window_count
if max_in_window > n:
frequent_users.append(user_id)
return frequent_users
# =====================================================================# 2. QUEUEING SYSTEM SIMULATION# =====================================================================def simulate_queue(arrivals: List[float], service_times: List[float]) -> float:
"""
Simulates a single-server First-In, First-Out (FIFO) queueing loop.
Returns the average wait time for all arriving processes.
"""
if not arrivals:
return 0.0
total_wait_time = 0.0
current_time = 0.0
for arrival, service in zip(arrivals, service_times):
# Server starts processing when the job arrives or when the server finishes previous task
start_time = max(arrival, current_time)
wait_time = start_time - arrival
total_wait_time += wait_time
# Advance clock by the duration of active service execution
current_time = start_time + service
return total_wait_time / len(arrivals)
# =====================================================================# 3. ROLLING MEDIAN ANOMALY DETECTION# =====================================================================def detect_anomalies(values: List[float], k: int, threshold: float) -> List[bool]:
"""
Detects anomalies by comparing each value to the median of the previous k values.
threshold: Maximum allowable absolute difference deviation boundary from the rolling median.
"""
anomalies = []
window = deque(maxlen=k)
for val in values:
if len(window) < k:
# Not enough historical lookback context to flags anomalies
anomalies.append(False)
else:
# Compute the rolling median over historical context window
sorted_window = sorted(list(window))
mid = k // 2
if k % 2 == 1:
median = sorted_window[mid]
else:
median = (sorted_window[mid - 1] + sorted_window[mid]) / 2.0
# Flag item if it steps past absolute threshold boundaries
is_anomaly = abs(val - median) > threshold
anomalies.append(is_anomaly)
window.append(val)
return anomalies
# =====================================================================# 4. CATEGORY REVIEW GROUPING# =====================================================================def top_rated_by_category(reviews: List[Dict[str, Any]]) -> Dict[str, List[str]]:
"""
Groups product reviews by category and returns the highest-rated product(s).
reviews: List of dicts, e.g., [{"product": "A", "category": "Tech", "rating": 4.8}]
"""
product_ratings = defaultdict(list)
product_category = {}
# Map raw records to grouped state
for r in reviews:
prod = r["product"]
cat = r["category"]
rating = r["rating"]
product_ratings[prod].append(rating)
product_category[prod] = cat
# Aggregate to calculate mean rating evaluations
avg_ratings = {prod: sum(rat)/len(rat) for prod, rat in product_ratings.items()}
category_groups = defaultdict(list)
for prod, avg_rating in avg_ratings.items():
cat = product_category[prod]
category_groups[cat].append((prod, avg_rating))
result = {}
for cat, prods in category_groups.items():
# Find highest rating score within this specific category subset
max_rating = max(prods, key=lambda x: x[1])[1]
# Select product keys matching maximum bounds to catch duplicates or ties
top_prods = [p[0] for p in prods if p[1] == max_rating]
result[cat] = top_prods
return result
# =====================================================================# DEMO EXECUTION# =====================================================================if __name__ == "__main__":
print("--- 1. Purchase Filter ---")
purchases = [
("user1", "2026-01-01"),
("user1", "2026-01-15"),
("user1", "2026-01-25"),
("user2", "2026-01-01")
]
print("Frequent Users (N=2):", get_frequent_users(purchases, n=2))
print("\n--- 2. Queue Simulation ---")
# Job 1 arrives at 0.0 -> processes immediately (wait=0.0) -> finishes at 5.0
# Job 2 arrives at 2.0 -> waits until 5.0 (wait=3.0) -> finishes at 9.0
print("Avg Wait Time:", simulate_queue(arrivals=[0.0, 2.0], service_times=[5.0, 4.0]))
print("\n--- 3. Anomaly Detection ---")
stream = [10.0, 12.0, 11.0, 13.0, 100.0, 12.0, 11.0]
print("Anomalies (k=4, thresh=15):", detect_anomalies(stream, k=4, threshold=15.0))
print("\n--- 4. Review Grouping ---")
sample_reviews = [
{"product": "Phone X", "category": "Tech", "rating": 5},
{"product": "Phone X", "category": "Tech", "rating": 4}, # Mean = 4.5
{"product": "Laptop Y", "category": "Tech", "rating": 5}, # Mean = 5.0
{"product": "Shirt Z", "category": "Apparel", "rating": 4} # Mean = 4.0
]
print("Top Products by Category:", top_rated_by_category(sample_reviews))
If you are dealing with performance constraints, tell me if you want to optimize the rolling median using dual min/max heaps to scale down execution complexity from $O(k \log k)$ to $O(\log k)$.
MEV bot development is increasingly becoming a key part of DeFi infrastructure as blockchain networks grow more competitive and transaction volumes continue to rise. These bots help identify and execute opportunities such as arbitrage, liquidity optimization, and transaction prioritization, enabling market participants to operate more efficiently in fast-moving decentralized environments. As a result, MEV technology is no longer viewed solely as a trading tool but as an important component of the broader DeFi ecosystem. As demand for sophisticated on-chain automation grows, businesses are looking for reliable MEV bot development solutions that combine speed, scalability, and security. For organizations seeking to build high-performance MEV systems, Softean is a leading MEV bot development service provider with expertise in creating customized solutions tailored to evolving DeFi and blockchain market requirements. Read More >> https://www.softean.com/mev-bot-development
from typing import List
from collections import deque
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# Build adjacency list: b -> list of courses that depend on b
adj = [[] for _ in range(numCourses)]
# indegree[x] = number of prerequisites for course x
indegree = [0] * numCourses
for a, b in prerequisites:
adj[b].append(a)
indegree[a] += 1
# Queue courses that currently have no prerequisites
q = deque()
for c in range(numCourses):
if indegree[c] == 0:
q.append(c)
taken = 0 # count processed courses
# Remove prerequisites layer by layer
while q:
course = q.popleft()
taken += 1
# Taking 'course' reduces indegree of its dependent courses
for nxt in adj[course]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
q.append(nxt)
# If we processed all courses, no cycle exists
return taken == numCourses
The rapid growth of cryptocurrency adoption has encouraged more entrepreneurs to launch their own trading platforms without the time and expense of building an exchange from scratch. A Coinbase Clone Script provides a faster route to market with essential features such as secure wallet integration, advanced trading functionality, KYC/AML support, scalable architecture, and a user-friendly interface, making it an attractive choice for businesses entering the crypto space. For businesses looking to capitalize on this growing opportunity, Coinexra's Coinbase Clone Script offers a fully customizable, enterprise-grade solution designed for performance, security, and scalability. With advanced features, seamless deployment, and end-to-end support, Coinexra helps entrepreneurs launch a competitive crypto exchange with confidence. Read More >> https://www.coinexra.com/coinbase-clone-script
Build your sports betting business with a custom 1xBet clone script designed around your operational needs. Connect reliable sports data sources and create a platform that supports efficient management, streamlined operations, and business growth.
For most founders, this comes down to one thing, speed versus control. Building from scratch gives you full control, but it takes months of development, a skilled blockchain team, and a higher budget. You are responsible for everything, from security and multi-chain support to testing and maintenance. A Trust Wallet clone script, on the other hand, gives you a ready foundation. Core features like wallet management, token swaps, and integrations are already built. You can customize it for your brand and go live much faster. From a business perspective, the advantage is clear. A clone script reduces development time and cost, and lets you focus on user growth and revenue instead of backend complexity. Building from scratch only makes sense if you have a large budget and a very specific product vision. If your goal is to launch quickly and scale efficiently, buying a Trust Wallet clone script is the more practical choice. Coinexra stands out as the best Trust Wallet clone script provider, offering reliable and customizable solutions built for real business use. >> https://www.coinexra.com/trust-wallet-clone
Sat Sep 12 2026 02:08:03 GMT+0000 (Coordinated Universal Time) https://onlineccompiler.com/
Thu Sep 03 2026 09:32:29 GMT+0000 (Coordinated Universal Time) https://bidbits.org/blog/crypto-forex-trading-platform-development
Thu Sep 03 2026 07:22:36 GMT+0000 (Coordinated Universal Time) https://bidbits.org/blog/sniper-bot-development
Mon Aug 31 2026 09:17:00 GMT+0000 (Coordinated Universal Time) https://maticz.com/crypto-wallet-development
Thu Aug 27 2026 04:58:28 GMT+0000 (Coordinated Universal Time) https://www.coinexra.com/xt-com-clone-script
Wed Aug 26 2026 18:05:34 GMT+0000 (Coordinated Universal Time) https://docs.gearset.com/en/articles/2439855-changes-to-flows-in-v44-of-the-metadata-api
Mon Aug 24 2026 21:58:27 GMT+0000 (Coordinated Universal Time) https://websitetoapk.com/docs/pushadmin-api-access.html
Mon Aug 24 2026 21:58:06 GMT+0000 (Coordinated Universal Time) https://websitetoapk.com/docs/pushadmin-api-access.html
Mon Aug 24 2026 21:57:43 GMT+0000 (Coordinated Universal Time) https://websitetoapk.com/docs/pushadmin-api-access.html
Mon Aug 24 2026 21:37:12 GMT+0000 (Coordinated Universal Time) https://websitetoapk.com/docs/creating-keystore-for-signing.html
Tue Aug 18 2026 06:07:44 GMT+0000 (Coordinated Universal Time) https://www.softean.com/binary-options-trading-software-development
Mon Aug 17 2026 11:47:23 GMT+0000 (Coordinated Universal Time) https://maticz.com/real-world-asset-tokenization
Thu Aug 13 2026 09:45:57 GMT+0000 (Coordinated Universal Time) https://www.coinexra.com/white-label-tokenization-platform
Wed Aug 12 2026 17:33:41 GMT+0000 (Coordinated Universal Time) https://hamza-sehouli.com/blog
Wed Aug 12 2026 07:48:46 GMT+0000 (Coordinated Universal Time) https://www.rginfotech.com/blog/top-casino-game-development-companies-usa/
Wed Aug 12 2026 04:50:36 GMT+0000 (Coordinated Universal Time) https://www.sunbedbooster.com/menshealth/viagra-super-active-100mg-order-online
Fri Aug 07 2026 07:27:56 GMT+0000 (Coordinated Universal Time) https://www.yumeustechnologies.com/nowpayments-clone-script
Mon Aug 03 2026 10:00:05 GMT+0000 (Coordinated Universal Time) https://www.softean.com/mev-bot-development
Mon Aug 03 2026 07:58:31 GMT+0000 (Coordinated Universal Time) https://cloneappz.com/amazon-clone-development
Fri Jul 31 2026 10:19:19 GMT+0000 (Coordinated Universal Time) https://www.coinexra.com/coinbase-clone-script
Wed Jul 29 2026 13:03:36 GMT+0000 (Coordinated Universal Time) https://www.firebeetechnoservices.com/blog/1xbet-clone
Wed Jul 29 2026 06:33:23 GMT+0000 (Coordinated Universal Time) https://www.coinexra.com/trust-wallet-clone


