feat: automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
Implementa las 4 fases de la especificación de automatización: módulo de plantillas/tarifas editable por el equipo, generación de PDF (HTML+JS vía Chrome headless) para cotizaciones/contratos/arquitecturas/cuentas de cobro, chat propio en el dashboard reutilizando el mismo motor y tools del bot de Telegram, y nuevas tools del agente para crear estos documentos end-to-end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
772a4a5656
commit
516220a8a1
+5
-3
@@ -3,7 +3,7 @@
|
|||||||
# Los assets del frontend (public/) se compilan localmente
|
# Los assets del frontend (public/) se compilan localmente
|
||||||
# antes del push con: npm run production
|
# antes del push con: npm run production
|
||||||
# ──────────────────────────────────────────
|
# ──────────────────────────────────────────
|
||||||
FROM golang:1.25-alpine AS builder
|
FROM golang:1.26-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Instalar dependencias del sistema necesarias para CGO (argon2, etc.)
|
# Instalar dependencias del sistema necesarias para CGO (argon2, etc.)
|
||||||
@@ -29,8 +29,10 @@ RUN cd agent && go mod download && \
|
|||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Certificados TLS y timezone
|
# Certificados TLS, timezone y Chromium headless (motor de generación de PDF
|
||||||
RUN apk add --no-cache ca-certificates tzdata
|
# para cotizaciones, contratos, actas y cuentas de cobro — ver pkg/services/pdf_service.go)
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata chromium
|
||||||
|
ENV CHROME_EXEC_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
# Copiar binario compilado
|
# Copiar binario compilado
|
||||||
COPY --from=builder /app/apiv2 .
|
COPY --from=builder /app/apiv2 .
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/sujit-baniya/fiber-boilerplate
|
module github.com/sujit-baniya/fiber-boilerplate
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
@@ -25,12 +25,11 @@ require (
|
|||||||
github.com/pyroscope-io/pyroscope v0.37.2
|
github.com/pyroscope-io/pyroscope v0.37.2
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||||
github.com/sujit-baniya/flash v0.1.9
|
github.com/sujit-baniya/flash v0.1.9
|
||||||
github.com/sujit-baniya/ip v0.0.10
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0
|
github.com/valyala/bytebufferpool v1.0.0
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
golang.org/x/crypto v0.51.0
|
golang.org/x/crypto v0.51.0
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
@@ -43,6 +42,8 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||||
github.com/chai2010/webp v1.4.0
|
github.com/chai2010/webp v1.4.0
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe
|
||||||
|
github.com/chromedp/chromedp v0.16.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
@@ -66,6 +67,7 @@ require (
|
|||||||
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||||
github.com/casbin/govaluate v1.2.0 // indirect
|
github.com/casbin/govaluate v1.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -73,12 +75,16 @@ require (
|
|||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-test/deep v1.1.0 // indirect
|
github.com/go-test/deep v1.1.0 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
@@ -116,7 +122,6 @@ require (
|
|||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pyroscope-io/dotnetdiag v1.2.1 // indirect
|
github.com/pyroscope-io/dotnetdiag v1.2.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
|||||||
@@ -95,8 +95,6 @@ github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6Ct
|
|||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
@@ -137,6 +135,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
||||||
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe h1:PmhRwLZ8qLtldQCBiydwdPFJI8WVQ936ux1cpgHLRb8=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0=
|
||||||
|
github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk=
|
||||||
|
github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U=
|
||||||
|
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
|
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
@@ -206,6 +210,8 @@ github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7b
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
@@ -236,9 +242,14 @@ github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncV
|
|||||||
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
|
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
|
||||||
github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o=
|
github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o=
|
||||||
github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc=
|
github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofiber/fiber/v2 v2.1.0/go.mod h1:aG+lMkwy3LyVit4CnmYUbUdgjpc3UYOltvlJZ78rgQ0=
|
github.com/gofiber/fiber/v2 v2.1.0/go.mod h1:aG+lMkwy3LyVit4CnmYUbUdgjpc3UYOltvlJZ78rgQ0=
|
||||||
github.com/gofiber/fiber/v2 v2.9.0/go.mod h1:Ah3IJikrKNRepl/HuVawppS25X7FWohwfCSRn7kJG28=
|
|
||||||
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
github.com/gofiber/fiber/v2 v2.44.0/go.mod h1:VTMtb/au8g01iqvHyaCzftuM/xmZgKOZCtFzz6CdV9w=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
@@ -439,9 +450,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
|||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
|
||||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
@@ -466,6 +474,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
@@ -556,9 +566,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
|||||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||||
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
@@ -669,8 +678,6 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
|||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/sujit-baniya/flash v0.1.9 h1:9+g9GrauXF8JH7ivoG4XBjl04r+vNkB9ZFN+5RKTugo=
|
github.com/sujit-baniya/flash v0.1.9 h1:9+g9GrauXF8JH7ivoG4XBjl04r+vNkB9ZFN+5RKTugo=
|
||||||
github.com/sujit-baniya/flash v0.1.9/go.mod h1:nNyzxxxU11ctsPe99BhV39kP6/jcfoRDp4T7P5m4K4E=
|
github.com/sujit-baniya/flash v0.1.9/go.mod h1:nNyzxxxU11ctsPe99BhV39kP6/jcfoRDp4T7P5m4K4E=
|
||||||
github.com/sujit-baniya/ip v0.0.10 h1:4FxdNgibzkDGS6B1+2//o4nStxmvD11i3NEgljh6SLk=
|
|
||||||
github.com/sujit-baniya/ip v0.0.10/go.mod h1:9wktAv0khHVF2t6lIWRbNV4ScqcHSi92pc5RfBb9YV8=
|
|
||||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
@@ -687,8 +694,6 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||||
github.com/valyala/fasthttp v1.23.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
|
|
||||||
github.com/valyala/fasthttp v1.24.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
|
|
||||||
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||||
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
|
github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
|
||||||
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
|
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
|
||||||
@@ -753,7 +758,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
@@ -847,7 +851,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
@@ -924,7 +927,6 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -952,7 +954,6 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -987,9 +988,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ func main() {
|
|||||||
// Tablero de tareas
|
// Tablero de tareas
|
||||||
&models.Tarea{},
|
&models.Tarea{},
|
||||||
&models.TareaComentario{},
|
&models.TareaComentario{},
|
||||||
|
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
|
||||||
|
&models.PlantillaDocumento{},
|
||||||
|
&models.Tarifa{},
|
||||||
|
&models.DocumentoGenerado{},
|
||||||
|
&models.Arquitectura{},
|
||||||
)
|
)
|
||||||
// Seed automático (idempotente) de módulos del sistema
|
// Seed automático (idempotente) de módulos del sistema
|
||||||
migrations.SeedRenovaciones()
|
migrations.SeedRenovaciones()
|
||||||
@@ -138,6 +143,7 @@ func main() {
|
|||||||
migrations.SeedWebSms()
|
migrations.SeedWebSms()
|
||||||
migrations.SeedUrlMonitor()
|
migrations.SeedUrlMonitor()
|
||||||
migrations.SeedTareas()
|
migrations.SeedTareas()
|
||||||
|
migrations.SeedAutomatizacionIA()
|
||||||
// Cargar config SMTP desde BD (sobreescribe valores del .env/config.yml si hay registro activo)
|
// Cargar config SMTP desde BD (sobreescribe valores del .env/config.yml si hay registro activo)
|
||||||
if smtpCfg, err := models.GetSmtpConfig(); err == nil {
|
if smtpCfg, err := models.GetSmtpConfig(); err == nil {
|
||||||
app.Http.Mail.Host = smtpCfg.Host
|
app.Http.Mail.Host = smtpCfg.Host
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ func Migrate() {
|
|||||||
// Tablero de tareas
|
// Tablero de tareas
|
||||||
&models.Tarea{},
|
&models.Tarea{},
|
||||||
&models.TareaComentario{},
|
&models.TareaComentario{},
|
||||||
|
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
|
||||||
|
&models.PlantillaDocumento{},
|
||||||
|
&models.Tarifa{},
|
||||||
|
&models.DocumentoGenerado{},
|
||||||
|
&models.Arquitectura{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatalf("Error during main migration: %v", err)
|
log.Fatalf("Error during main migration: %v", err)
|
||||||
}
|
}
|
||||||
@@ -150,6 +155,9 @@ func Migrate() {
|
|||||||
// Agregar submódulo "Partner Recursos" al módulo Portal de Clientes
|
// Agregar submódulo "Partner Recursos" al módulo Portal de Clientes
|
||||||
SeedPartnerRecursos()
|
SeedPartnerRecursos()
|
||||||
|
|
||||||
|
// Crear módulo y submódulos de Automatización IA (cotizaciones, contratos, asistente)
|
||||||
|
SeedAutomatizacionIA()
|
||||||
|
|
||||||
log.Println("Migration Completed...")
|
log.Println("Migration Completed...")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +412,79 @@ func SeedRenovaciones() {
|
|||||||
log.Println("[SEED] Seed de Renovaciones completado.")
|
log.Println("[SEED] Seed de Renovaciones completado.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeedAutomatizacionIA inserta el módulo "Automatización IA" y sus submódulos
|
||||||
|
// (Asistente, Plantillas de Documento, Tarifas) si no existen, y los asigna al
|
||||||
|
// rol Administrador. Es idempotente.
|
||||||
|
func SeedAutomatizacionIA() {
|
||||||
|
db := app.Http.Database.DB
|
||||||
|
|
||||||
|
// 1. Crear/obtener módulo
|
||||||
|
var modulo models.Modules
|
||||||
|
result := db.Where("title = ?", "Automatización IA").First(&modulo)
|
||||||
|
if result.Error != nil {
|
||||||
|
modulo = models.Modules{
|
||||||
|
Title: "Automatización IA",
|
||||||
|
Description: "Cotizaciones, contratos, actas y cuentas de cobro generados por IA",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := db.Create(&modulo).Error; err != nil {
|
||||||
|
log.Printf("[SEED] Error creando módulo Automatización IA: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[SEED] Módulo 'Automatización IA' creado con ID %d", modulo.ID)
|
||||||
|
} else {
|
||||||
|
log.Printf("[SEED] Módulo 'Automatización IA' ya existe (ID %d)", modulo.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Definir submódulos
|
||||||
|
entries := []struct{ title, desc, url string }{
|
||||||
|
{"Asistente", "Chat propio: mismo motor y tools que el bot de Telegram", "/app/asistente"},
|
||||||
|
{"Plantillas de Documento", "Fuente de verdad de cotización, contrato, acta y cuenta de cobro", "/app/automatizacion/plantillas"},
|
||||||
|
{"Tarifas", "Valor por hora, licencias, VMs y márgenes usados al cotizar", "/app/automatizacion/tarifas"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var insertados []models.Submodules
|
||||||
|
for _, e := range entries {
|
||||||
|
var sub models.Submodules
|
||||||
|
if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil {
|
||||||
|
sub = models.Submodules{
|
||||||
|
Title: e.title,
|
||||||
|
Description: e.desc,
|
||||||
|
Url: e.url,
|
||||||
|
ModuleId: modulo.ID,
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := db.Create(&sub).Error; err != nil {
|
||||||
|
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID)
|
||||||
|
} else {
|
||||||
|
if sub.ModuleId != modulo.ID {
|
||||||
|
if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil {
|
||||||
|
log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[SEED] Submódulo '%s' reasignado a módulo Automatización IA", sub.Title)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insertados = append(insertados, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Asignar solo al rol Administrador
|
||||||
|
var rolAdmin models.Roles
|
||||||
|
if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil {
|
||||||
|
log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err)
|
||||||
|
} else {
|
||||||
|
db.Model(&rolAdmin).Association("Submodules").Append(&insertados)
|
||||||
|
log.Printf("[SEED] Submódulos de Automatización IA asignados al rol 'Administrador'")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[SEED] Seed de Automatización IA completado.")
|
||||||
|
}
|
||||||
|
|
||||||
// SeedIntegraciones crea el módulo "Integraciones" con los submódulos
|
// SeedIntegraciones crea el módulo "Integraciones" con los submódulos
|
||||||
// de Hostinger y Cloudflare, y los asigna a todos los roles existentes.
|
// de Hostinger y Cloudflare, y los asigna a todos los roles existentes.
|
||||||
// Es idempotente: si el registro ya existe (por URL) lo reutiliza sin duplicar.
|
// Es idempotente: si el registro ya existe (por URL) lo reutiliza sin duplicar.
|
||||||
|
|||||||
+14
-3
@@ -24,11 +24,11 @@ type AiConfig struct {
|
|||||||
// "" = global (disponible para todos como fallback)
|
// "" = global (disponible para todos como fallback)
|
||||||
// "landing" = exclusivo para Landing Generator
|
// "landing" = exclusivo para Landing Generator
|
||||||
// "query_runner" = exclusivo para Query Runner SQL
|
// "query_runner" = exclusivo para Query Runner SQL
|
||||||
Modulo string `gorm:"size:50;default:''" json:"modulo"`
|
Modulo string `gorm:"size:50;default:''" json:"modulo"`
|
||||||
// Agente Telegram: si EsAgenteBot=true, esta config es el cerebro del bot administrador.
|
// Agente Telegram: si EsAgenteBot=true, esta config es el cerebro del bot administrador.
|
||||||
// Solo debe haber una config activa como agente a la vez.
|
// Solo debe haber una config activa como agente a la vez.
|
||||||
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
||||||
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AiConfig) TableName() string { return "ai_configs" }
|
func (AiConfig) TableName() string { return "ai_configs" }
|
||||||
@@ -127,6 +127,17 @@ func GetAgenteBotConfig() (*AiConfig, *TelegramConfig, error) {
|
|||||||
return &ai, tg, nil
|
return &ai, tg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAgenteBotAiConfig retorna solo la config de IA marcada como agente (el mismo
|
||||||
|
// "cerebro" que usa el bot de Telegram), sin exigir que tenga un bot de Telegram
|
||||||
|
// asignado. La usa el chat propio del dashboard para compartir el mismo motor.
|
||||||
|
func GetAgenteBotAiConfig() (*AiConfig, error) {
|
||||||
|
var ai AiConfig
|
||||||
|
if err := app.Http.Database.DB.Where("es_agente_bot = ? AND is_active = ?", true, true).First(&ai).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("no hay agente configurado: %w", err)
|
||||||
|
}
|
||||||
|
return &ai, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAiConfigForService retorna la config activa asignada al módulo indicado.
|
// GetAiConfigForService retorna la config activa asignada al módulo indicado.
|
||||||
// Lógica de prioridad:
|
// Lógica de prioridad:
|
||||||
// 1. Config activa con modulo conteniendo service (puede ser comma-separated)
|
// 1. Config activa con modulo conteniendo service (puede ser comma-separated)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Arquitectura guarda patrones de arquitectura técnica ya resueltos (ej: Active
|
||||||
|
// Directory redundante en Azure con VPN Gateway) para que la IA los reutilice como
|
||||||
|
// referencia al generar una propuesta técnica nueva, en vez de improvisar cada vez.
|
||||||
|
type Arquitectura struct {
|
||||||
|
gorm.Model
|
||||||
|
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||||
|
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||||
|
ContenidoHTML string `json:"contenido_html" gorm:"column:contenido_html;type:text"` // diagrama/detalle en HTML, reutilizable como referencia
|
||||||
|
Tags string `json:"tags" gorm:"column:tags;size:255"` // comma-separated, ej: "azure,ad,vpn"
|
||||||
|
EsReferencia bool `json:"es_referencia" gorm:"column:es_referencia;default:true"` // true = patrón reutilizable, false = propuesta generada puntual
|
||||||
|
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Arquitectura) TableName() string { return "arquitecturas" }
|
||||||
|
|
||||||
|
func GetAllArquitecturas(limit, offset int, search string, soloReferencias bool) ([]Arquitectura, int64, error) {
|
||||||
|
var items []Arquitectura
|
||||||
|
var total int64
|
||||||
|
db := app.Http.Database.DB.Model(&Arquitectura{})
|
||||||
|
if search != "" {
|
||||||
|
db = db.Where("nombre ILIKE ? OR tags ILIKE ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
}
|
||||||
|
if soloReferencias {
|
||||||
|
db = db.Where("es_referencia = ?", true)
|
||||||
|
}
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := db.Order("nombre ASC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetArquitecturaByID(id uint) (*Arquitectura, error) {
|
||||||
|
var item Arquitectura
|
||||||
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateArquitectura(a *Arquitectura) error {
|
||||||
|
return app.Http.Database.DB.Create(a).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateArquitectura(id uint, updates map[string]interface{}) error {
|
||||||
|
return app.Http.Database.DB.Model(&Arquitectura{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteArquitectura(id uint) error {
|
||||||
|
return app.Http.Database.DB.Delete(&Arquitectura{}, id).Error
|
||||||
|
}
|
||||||
+93
-66
@@ -61,19 +61,19 @@ func (Transaccion) TableName() string { return "contab_transacciones" }
|
|||||||
|
|
||||||
type CuentaCobro struct {
|
type CuentaCobro struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"`
|
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"`
|
||||||
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||||
EntidadID *uint `json:"entidad_id" gorm:"column:entidad_id;index"`
|
EntidadID *uint `json:"entidad_id" gorm:"column:entidad_id;index"`
|
||||||
Entidad *Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
Entidad *Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||||
FechaVencimiento *time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
FechaVencimiento *time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
||||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
||||||
@@ -82,17 +82,17 @@ func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
|||||||
|
|
||||||
type CuentaPagar struct {
|
type CuentaPagar struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
EntidadID uint `json:"entidad_id" gorm:"column:entidad_id;index;not null"`
|
EntidadID uint `json:"entidad_id" gorm:"column:entidad_id;index;not null"`
|
||||||
Entidad Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
Entidad Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||||
Vencimiento *time.Time `json:"vencimiento" gorm:"column:vencimiento"`
|
Vencimiento *time.Time `json:"vencimiento" gorm:"column:vencimiento"`
|
||||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
||||||
@@ -101,12 +101,12 @@ func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
|||||||
|
|
||||||
type ConsolidadoMensual struct {
|
type ConsolidadoMensual struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Anio int `json:"anio" gorm:"column:anio;not null"`
|
Anio int `json:"anio" gorm:"column:anio;not null"`
|
||||||
Mes int `json:"mes" gorm:"column:mes;not null"`
|
Mes int `json:"mes" gorm:"column:mes;not null"`
|
||||||
TotalIngresos float64 `json:"total_ingresos" gorm:"column:total_ingresos;default:0"`
|
TotalIngresos float64 `json:"total_ingresos" gorm:"column:total_ingresos;default:0"`
|
||||||
TotalEgresos float64 `json:"total_egresos" gorm:"column:total_egresos;default:0"`
|
TotalEgresos float64 `json:"total_egresos" gorm:"column:total_egresos;default:0"`
|
||||||
TotalRetenciones float64 `json:"total_retenciones" gorm:"column:total_retenciones;default:0"`
|
TotalRetenciones float64 `json:"total_retenciones" gorm:"column:total_retenciones;default:0"`
|
||||||
Resultado float64 `json:"resultado" gorm:"column:resultado;default:0"`
|
Resultado float64 `json:"resultado" gorm:"column:resultado;default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ConsolidadoMensual) TableName() string { return "contab_consolidado" }
|
func (ConsolidadoMensual) TableName() string { return "contab_consolidado" }
|
||||||
@@ -307,7 +307,11 @@ func SeedBalanceData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── FACTURAS → Transacciones (ingresos) ────────────────────────────────
|
// ─── FACTURAS → Transacciones (ingresos) ────────────────────────────────
|
||||||
facturas := []struct{ factura float64; valor float64; fecha string }{
|
facturas := []struct {
|
||||||
|
factura float64
|
||||||
|
valor float64
|
||||||
|
fecha string
|
||||||
|
}{
|
||||||
{54, 450000, "2026-01-01 00:00:00"},
|
{54, 450000, "2026-01-01 00:00:00"},
|
||||||
{55, 9000000, "2026-01-01 00:00:00"},
|
{55, 9000000, "2026-01-01 00:00:00"},
|
||||||
{56, 119000, "2026-01-01 00:00:00"},
|
{56, 119000, "2026-01-01 00:00:00"},
|
||||||
@@ -347,7 +351,7 @@ func SeedBalanceData() {
|
|||||||
if db.Where("descripcion = ?", "Ajuste/NC ene 2026").First(&existing).Error != nil {
|
if db.Where("descripcion = ?", "Ajuste/NC ene 2026").First(&existing).Error != nil {
|
||||||
db.Create(&Transaccion{
|
db.Create(&Transaccion{
|
||||||
Fecha: parseDate("2026-01-01 00:00:00"),
|
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||||
Tipo: "egreso", Descripcion: "Ajuste/NC ene 2026",
|
Tipo: "egreso", Descripcion: "Ajuste/NC ene 2026",
|
||||||
Valor: 130000, CuentaID: cuentaIng, Estado: "registrada",
|
Valor: 130000, CuentaID: cuentaIng, Estado: "registrada",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -369,7 +373,11 @@ func SeedBalanceData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── IVA → Transacciones (egresos) ──────────────────────────────────────
|
// ─── IVA → Transacciones (egresos) ──────────────────────────────────────
|
||||||
ivas := []struct{ valor float64; entidad string; desc string }{
|
ivas := []struct {
|
||||||
|
valor float64
|
||||||
|
entidad string
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
{285000, "DOCUXER", "IVA Fact #63"},
|
{285000, "DOCUXER", "IVA Fact #63"},
|
||||||
{503500, "DOCUXER", "IVA Fact #70"},
|
{503500, "DOCUXER", "IVA Fact #70"},
|
||||||
{19000, "GIAF SAS", "IVA Fact #56"},
|
{19000, "GIAF SAS", "IVA Fact #56"},
|
||||||
@@ -383,7 +391,7 @@ func SeedBalanceData() {
|
|||||||
eid := getEntidad(iv.entidad)
|
eid := getEntidad(iv.entidad)
|
||||||
db.Create(&Transaccion{
|
db.Create(&Transaccion{
|
||||||
Fecha: parseDate("2026-01-01 00:00:00"),
|
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||||
Tipo: "egreso", Descripcion: iv.desc,
|
Tipo: "egreso", Descripcion: iv.desc,
|
||||||
Valor: iv.valor, CuentaID: cuentaImp,
|
Valor: iv.valor, CuentaID: cuentaImp,
|
||||||
EntidadID: eid, Estado: "registrada",
|
EntidadID: eid, Estado: "registrada",
|
||||||
})
|
})
|
||||||
@@ -391,7 +399,11 @@ func SeedBalanceData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── RETENCION → Transacciones (egresos) ────────────────────────────────
|
// ─── RETENCION → Transacciones (egresos) ────────────────────────────────
|
||||||
rets := []struct{ valor float64; fecha string; estado string }{
|
rets := []struct {
|
||||||
|
valor float64
|
||||||
|
fecha string
|
||||||
|
estado string
|
||||||
|
}{
|
||||||
{195000, "2026-03-01 00:00:00", "pagado"},
|
{195000, "2026-03-01 00:00:00", "pagado"},
|
||||||
{257000, "2026-02-01 00:00:00", "pagado"},
|
{257000, "2026-02-01 00:00:00", "pagado"},
|
||||||
{431000, "2026-01-01 00:00:00", "pendiente"},
|
{431000, "2026-01-01 00:00:00", "pendiente"},
|
||||||
@@ -402,7 +414,7 @@ func SeedBalanceData() {
|
|||||||
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
||||||
db.Create(&Transaccion{
|
db.Create(&Transaccion{
|
||||||
Fecha: parseDate(r.fecha),
|
Fecha: parseDate(r.fecha),
|
||||||
Tipo: "egreso", Descripcion: desc,
|
Tipo: "egreso", Descripcion: desc,
|
||||||
Valor: r.valor, CuentaID: cuentaImp,
|
Valor: r.valor, CuentaID: cuentaImp,
|
||||||
Estado: "registrada",
|
Estado: "registrada",
|
||||||
})
|
})
|
||||||
@@ -410,7 +422,11 @@ func SeedBalanceData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── CUENTAS DE COBRO (las que te pasan a ti) → CuentasPagar ────────────
|
// ─── CUENTAS DE COBRO (las que te pasan a ti) → CuentasPagar ────────────
|
||||||
cobros := []struct{ entidad string; valor float64; fecha string }{
|
cobros := []struct {
|
||||||
|
entidad string
|
||||||
|
valor float64
|
||||||
|
fecha string
|
||||||
|
}{
|
||||||
{"NATALIA", 2000000, "2026-01-01 00:00:00"},
|
{"NATALIA", 2000000, "2026-01-01 00:00:00"},
|
||||||
{"FELIPE", 2000000, "2026-01-01 00:00:00"},
|
{"FELIPE", 2000000, "2026-01-01 00:00:00"},
|
||||||
{"CONTADORA", 1780000, "2026-01-01 00:00:00"},
|
{"CONTADORA", 1780000, "2026-01-01 00:00:00"},
|
||||||
@@ -439,7 +455,10 @@ func SeedBalanceData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── CONSOLIDADO POR MES ───────────────────────────────────────────────
|
// ─── CONSOLIDADO POR MES ───────────────────────────────────────────────
|
||||||
consols := []struct{ mes int; ing, egre, resul float64 }{
|
consols := []struct {
|
||||||
|
mes int
|
||||||
|
ing, egre, resul float64
|
||||||
|
}{
|
||||||
{1, 12669000, 8467849, 4201151},
|
{1, 12669000, 8467849, 4201151},
|
||||||
{2, 8470500, 4300000, 4170500},
|
{2, 8470500, 4300000, 4170500},
|
||||||
{3, 2109000, 4300000, -2191000},
|
{3, 2109000, 4300000, -2191000},
|
||||||
@@ -459,6 +478,14 @@ func SeedBalanceData() {
|
|||||||
log.Println("[SEED] Balance data imported from BALANCE.numbers")
|
log.Println("[SEED] Balance data imported from BALANCE.numbers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCuentaCobroByID(id uint) (*CuentaCobro, error) {
|
||||||
|
var item CuentaCobro
|
||||||
|
if err := app.Http.Database.DB.Preload("Cliente").Preload("Entidad").First(&item, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CreateCuentaCobro(cc *CuentaCobro) error {
|
func CreateCuentaCobro(cc *CuentaCobro) error {
|
||||||
return app.Http.Database.DB.Create(cc).Error
|
return app.Http.Database.DB.Create(cc).Error
|
||||||
}
|
}
|
||||||
@@ -565,12 +592,12 @@ func CalcularYGuardarConsolidado(mes, anio int) (*ConsolidadoMensual, error) {
|
|||||||
resultado := ingresos.Total - egresos.Total
|
resultado := ingresos.Total - egresos.Total
|
||||||
|
|
||||||
c := &ConsolidadoMensual{
|
c := &ConsolidadoMensual{
|
||||||
Anio: anio,
|
Anio: anio,
|
||||||
Mes: mes,
|
Mes: mes,
|
||||||
TotalIngresos: ingresos.Total,
|
TotalIngresos: ingresos.Total,
|
||||||
TotalEgresos: egresos.Total,
|
TotalEgresos: egresos.Total,
|
||||||
TotalRetenciones: 0,
|
TotalRetenciones: 0,
|
||||||
Resultado: resultado,
|
Resultado: resultado,
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing ConsolidadoMensual
|
var existing ConsolidadoMensual
|
||||||
@@ -605,15 +632,15 @@ func ListConsolidados(anio int) ([]ConsolidadoMensual, error) {
|
|||||||
// ─── Datos para dashboard ────────────────────────────────────────────────────
|
// ─── Datos para dashboard ────────────────────────────────────────────────────
|
||||||
|
|
||||||
type DashboardData struct {
|
type DashboardData struct {
|
||||||
Mes int `json:"mes"`
|
Mes int `json:"mes"`
|
||||||
Anio int `json:"anio"`
|
Anio int `json:"anio"`
|
||||||
TotalIngresos float64 `json:"total_ingresos"`
|
TotalIngresos float64 `json:"total_ingresos"`
|
||||||
TotalEgresos float64 `json:"total_egresos"`
|
TotalEgresos float64 `json:"total_egresos"`
|
||||||
Resultado float64 `json:"resultado"`
|
Resultado float64 `json:"resultado"`
|
||||||
CantTransacciones int64 `json:"cant_transacciones"`
|
CantTransacciones int64 `json:"cant_transacciones"`
|
||||||
PendientesCobro float64 `json:"pendientes_cobro"`
|
PendientesCobro float64 `json:"pendientes_cobro"`
|
||||||
PendientesPago float64 `json:"pendientes_pago"`
|
PendientesPago float64 `json:"pendientes_pago"`
|
||||||
Transacciones []Transaccion `json:"transacciones"`
|
Transacciones []Transaccion `json:"transacciones"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
||||||
@@ -657,15 +684,15 @@ func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
|||||||
|
|
||||||
func SeedContabilidad() {
|
func SeedContabilidad() {
|
||||||
cuentas := []Cuenta{
|
cuentas := []Cuenta{
|
||||||
{Codigo: "ING-FAC", Nombre: "Facturación", Tipo: "ingreso", Color: "#22c55e"},
|
{Codigo: "ING-FAC", Nombre: "Facturación", Tipo: "ingreso", Color: "#22c55e"},
|
||||||
{Codigo: "ING-OTR", Nombre: "Otros ingresos", Tipo: "ingreso", Color: "#16a34a"},
|
{Codigo: "ING-OTR", Nombre: "Otros ingresos", Tipo: "ingreso", Color: "#16a34a"},
|
||||||
{Codigo: "EGR-HOS", Nombre: "Hosting/Servidores", Tipo: "egreso", Color: "#ef4444"},
|
{Codigo: "EGR-HOS", Nombre: "Hosting/Servidores", Tipo: "egreso", Color: "#ef4444"},
|
||||||
{Codigo: "EGR-DOM", Nombre: "Dominios", Tipo: "egreso", Color: "#dc2626"},
|
{Codigo: "EGR-DOM", Nombre: "Dominios", Tipo: "egreso", Color: "#dc2626"},
|
||||||
{Codigo: "EGR-SRV", Nombre: "Servicios", Tipo: "egreso", Color: "#f97316"},
|
{Codigo: "EGR-SRV", Nombre: "Servicios", Tipo: "egreso", Color: "#f97316"},
|
||||||
{Codigo: "EGR-GRAL", Nombre: "Gastos generales", Tipo: "egreso", Color: "#eab308"},
|
{Codigo: "EGR-GRAL", Nombre: "Gastos generales", Tipo: "egreso", Color: "#eab308"},
|
||||||
{Codigo: "EGR-NOM", Nombre: "Nómina", Tipo: "egreso", Color: "#a855f7"},
|
{Codigo: "EGR-NOM", Nombre: "Nómina", Tipo: "egreso", Color: "#a855f7"},
|
||||||
{Codigo: "EGR-IMPU", Nombre: "Impuestos", Tipo: "egreso", Color: "#6366f1"},
|
{Codigo: "EGR-IMPU", Nombre: "Impuestos", Tipo: "egreso", Color: "#6366f1"},
|
||||||
{Codigo: "EGR-MKT", Nombre: "Marketing", Tipo: "egreso", Color: "#ec4899"},
|
{Codigo: "EGR-MKT", Nombre: "Marketing", Tipo: "egreso", Color: "#ec4899"},
|
||||||
}
|
}
|
||||||
for _, c := range cuentas {
|
for _, c := range cuentas {
|
||||||
var existing Cuenta
|
var existing Cuenta
|
||||||
@@ -675,13 +702,13 @@ func SeedContabilidad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entidades := []Entidad{
|
entidades := []Entidad{
|
||||||
{Nombre: "DOCUXER", Tipo: "cliente"},
|
{Nombre: "DOCUXER", Tipo: "cliente"},
|
||||||
{Nombre: "GIAF SAS", Tipo: "cliente"},
|
{Nombre: "GIAF SAS", Tipo: "cliente"},
|
||||||
{Nombre: "TECZONE", Tipo: "proveedor"},
|
{Nombre: "TECZONE", Tipo: "proveedor"},
|
||||||
{Nombre: "FELIPE", Tipo: "proveedor"},
|
{Nombre: "FELIPE", Tipo: "proveedor"},
|
||||||
{Nombre: "NATALIA", Tipo: "proveedor"},
|
{Nombre: "NATALIA", Tipo: "proveedor"},
|
||||||
{Nombre: "CONTADORA", Tipo: "proveedor"},
|
{Nombre: "CONTADORA", Tipo: "proveedor"},
|
||||||
{Nombre: "ANDREMER", Tipo: "proveedor"},
|
{Nombre: "ANDREMER", Tipo: "proveedor"},
|
||||||
}
|
}
|
||||||
for _, e := range entidades {
|
for _, e := range entidades {
|
||||||
var existing Entidad
|
var existing Entidad
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocumentoGenerado es el historial de documentos producidos por el motor de
|
||||||
|
// automatización con IA (cotización, contrato, acta/arquitectura, cuenta de cobro),
|
||||||
|
// sin importar el canal que los originó (Telegram, chat propio o Claude directo).
|
||||||
|
type DocumentoGenerado struct {
|
||||||
|
gorm.Model
|
||||||
|
Tipo string `json:"tipo" gorm:"column:tipo;size:30;not null;index"` // cotizacion | contrato | acta | cuenta_cobro
|
||||||
|
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"`
|
||||||
|
Cliente *Cliente `json:"cliente,omitempty" gorm:"foreignKey:ClienteID"`
|
||||||
|
ProyectoID *uint `json:"proyecto_id" gorm:"column:proyecto_id;index"`
|
||||||
|
PlantillaID *uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
||||||
|
Nombre string `json:"nombre" gorm:"column:nombre;size:200"`
|
||||||
|
Archivo string `json:"archivo" gorm:"column:archivo"` // ruta en disco, ej: uploads/documentos/cotizacion/12/xyz.pdf
|
||||||
|
TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime;default:'application/pdf'"`
|
||||||
|
Tamanio int64 `json:"tamanio" gorm:"column:tamanio"`
|
||||||
|
DatosJSON string `json:"datos_json" gorm:"column:datos_json;type:text"` // input usado para generarlo (auditoría)
|
||||||
|
GeneradoPor string `json:"generado_por" gorm:"column:generado_por;size:20;default:'web'"` // web | telegram | claude_api
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DocumentoGenerado) TableName() string { return "documentos_generados" }
|
||||||
|
|
||||||
|
func CreateDocumentoGenerado(d *DocumentoGenerado) error {
|
||||||
|
return app.Http.Database.DB.Create(d).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllDocumentosGenerados(limit, offset int, tipo string, clienteID uint) ([]DocumentoGenerado, int64, error) {
|
||||||
|
var items []DocumentoGenerado
|
||||||
|
var total int64
|
||||||
|
db := app.Http.Database.DB.Model(&DocumentoGenerado{}).Preload("Cliente")
|
||||||
|
if tipo != "" {
|
||||||
|
db = db.Where("tipo = ?", tipo)
|
||||||
|
}
|
||||||
|
if clienteID != 0 {
|
||||||
|
db = db.Where("cliente_id = ?", clienteID)
|
||||||
|
}
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDocumentoGeneradoByID(id uint) (*DocumentoGenerado, error) {
|
||||||
|
var item DocumentoGenerado
|
||||||
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlantillaDocumento es la fuente de verdad de las plantillas base usadas por
|
||||||
|
// la automatización con IA: cotización, contrato, acta de proyecto y cuenta de cobro.
|
||||||
|
// Editar una plantilla aquí actualiza automáticamente todos los canales (Telegram,
|
||||||
|
// chat propio, Claude directo) que generan ese tipo de documento.
|
||||||
|
type PlantillaDocumento struct {
|
||||||
|
gorm.Model
|
||||||
|
Tipo string `json:"tipo" gorm:"column:tipo;size:30;not null;index"` // cotizacion | contrato | acta | cuenta_cobro
|
||||||
|
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||||
|
ContenidoHTML string `json:"contenido_html" gorm:"column:contenido_html;type:text"` // Go text/template
|
||||||
|
Version int `json:"version" gorm:"column:version;default:1"`
|
||||||
|
Activa bool `json:"activa" gorm:"column:activa;default:true"`
|
||||||
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PlantillaDocumento) TableName() string { return "plantillas_documento" }
|
||||||
|
|
||||||
|
func GetAllPlantillasDocumento(limit, offset int, search, tipo string) ([]PlantillaDocumento, int64, error) {
|
||||||
|
var items []PlantillaDocumento
|
||||||
|
var total int64
|
||||||
|
db := app.Http.Database.DB.Model(&PlantillaDocumento{})
|
||||||
|
if search != "" {
|
||||||
|
db = db.Where("nombre ILIKE ?", "%"+search+"%")
|
||||||
|
}
|
||||||
|
if tipo != "" {
|
||||||
|
db = db.Where("tipo = ?", tipo)
|
||||||
|
}
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := db.Order("tipo ASC, version DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlantillaDocumentoByID(id uint) (*PlantillaDocumento, error) {
|
||||||
|
var item PlantillaDocumento
|
||||||
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlantillaDocumentoActiva retorna la plantilla activa más reciente para un tipo dado.
|
||||||
|
// Es la que usan los endpoints de generación (cotizaciones, contratos, etc).
|
||||||
|
func GetPlantillaDocumentoActiva(tipo string) (*PlantillaDocumento, error) {
|
||||||
|
var item PlantillaDocumento
|
||||||
|
if err := app.Http.Database.DB.
|
||||||
|
Where("tipo = ? AND activa = ?", tipo, true).
|
||||||
|
Order("version DESC").
|
||||||
|
First(&item).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePlantillaDocumento(p PlantillaDocumento) error {
|
||||||
|
return app.Http.Database.DB.Create(&p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdatePlantillaDocumento(id uint, updates map[string]interface{}) error {
|
||||||
|
return app.Http.Database.DB.Model(&PlantillaDocumento{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePlantillaDocumento(id uint) error {
|
||||||
|
return app.Http.Database.DB.Delete(&PlantillaDocumento{}, id).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tarifa es la tabla de precios/reglas de negocio que la IA usa para calcular
|
||||||
|
// cotizaciones y contratos: valor por hora según tipo de servicio, licencias M365,
|
||||||
|
// tipos de VM Azure recurrentes, márgenes estándar, etc.
|
||||||
|
type Tarifa struct {
|
||||||
|
gorm.Model
|
||||||
|
Categoria string `json:"categoria" gorm:"column:categoria;size:50;not null;index"` // hora_servicio | licencia | vm_azure | margen | otro
|
||||||
|
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||||
|
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||||
|
Moneda string `json:"moneda" gorm:"column:moneda;size:10;default:'COP'"`
|
||||||
|
Unidad string `json:"unidad" gorm:"column:unidad;size:20"` // hora | mes | unico | porcentaje
|
||||||
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Tarifa) TableName() string { return "tarifas" }
|
||||||
|
|
||||||
|
func GetAllTarifas(limit, offset int, search, categoria string) ([]Tarifa, int64, error) {
|
||||||
|
var items []Tarifa
|
||||||
|
var total int64
|
||||||
|
db := app.Http.Database.DB.Model(&Tarifa{})
|
||||||
|
if search != "" {
|
||||||
|
db = db.Where("nombre ILIKE ?", "%"+search+"%")
|
||||||
|
}
|
||||||
|
if categoria != "" {
|
||||||
|
db = db.Where("categoria = ?", categoria)
|
||||||
|
}
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := db.Order("categoria ASC, nombre ASC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTarifasActivas retorna todas las tarifas activas, opcionalmente filtradas por categoría.
|
||||||
|
// Es lo que usan los endpoints de generación para calcular precios.
|
||||||
|
func GetTarifasActivas(categoria string) ([]Tarifa, error) {
|
||||||
|
var items []Tarifa
|
||||||
|
db := app.Http.Database.DB.Where("activo = ?", true)
|
||||||
|
if categoria != "" {
|
||||||
|
db = db.Where("categoria = ?", categoria)
|
||||||
|
}
|
||||||
|
if err := db.Order("nombre ASC").Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTarifaByID(id uint) (*Tarifa, error) {
|
||||||
|
var item Tarifa
|
||||||
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTarifa(t Tarifa) error {
|
||||||
|
return app.Http.Database.DB.Create(&t).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateTarifa(id uint, updates map[string]interface{}) error {
|
||||||
|
return app.Http.Database.DB.Model(&Tarifa{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTarifa(id uint) error {
|
||||||
|
return app.Http.Database.DB.Delete(&Tarifa{}, id).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerarArquitectura produce el PDF de una propuesta técnica a partir de un
|
||||||
|
// requerimiento en texto libre. Quien llama (Claude) ya consultó
|
||||||
|
// listar_arquitecturas_referencia para reutilizar patrones ya resueltos en vez de
|
||||||
|
// improvisar, y trae la propuesta ya armada; el backend solo la renderiza a PDF y,
|
||||||
|
// si aplica, la deja guardada como nueva referencia reutilizable.
|
||||||
|
func GenerarArquitectura(requerimiento, propuesta, nombre string, clienteID *uint, guardarComoReferencia bool, generadoPor string) (*models.DocumentoGenerado, error) {
|
||||||
|
if requerimiento == "" {
|
||||||
|
return nil, fmt.Errorf("requerimiento requerido")
|
||||||
|
}
|
||||||
|
if propuesta == "" {
|
||||||
|
return nil, fmt.Errorf("propuesta requerida")
|
||||||
|
}
|
||||||
|
if nombre == "" {
|
||||||
|
nombre = "Propuesta técnica"
|
||||||
|
}
|
||||||
|
|
||||||
|
var cliente *models.Cliente
|
||||||
|
if clienteID != nil {
|
||||||
|
c, err := models.GetClienteByID(*clienteID)
|
||||||
|
if err == nil {
|
||||||
|
cliente = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datos := DatosBaseDocumento(map[string]interface{}{
|
||||||
|
"Nombre": nombre,
|
||||||
|
"Requerimiento": requerimiento,
|
||||||
|
"Propuesta": propuesta,
|
||||||
|
"Cliente": cliente,
|
||||||
|
})
|
||||||
|
|
||||||
|
doc, _, err := GenerarDocumento("arquitectura", datos, clienteID, nil, generadoPor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if guardarComoReferencia {
|
||||||
|
ref := &models.Arquitectura{
|
||||||
|
Nombre: nombre,
|
||||||
|
Descripcion: requerimiento,
|
||||||
|
ContenidoHTML: propuesta,
|
||||||
|
EsReferencia: true,
|
||||||
|
ClienteID: clienteID,
|
||||||
|
}
|
||||||
|
_ = models.CreateArquitectura(ref) // no bloquea la generación del documento si falla
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerarDocumentoContrato produce el PDF de un contrato ya existente a partir de
|
||||||
|
// la plantilla activa tipo 'contrato' (cláusulas estándar) + los datos del contrato
|
||||||
|
// (cliente, servicios, vigencia, valor). Es la implementación única que comparten
|
||||||
|
// el endpoint POST /api/v2/contratos/:id/generar-documento y la tool crear_contrato.
|
||||||
|
func GenerarDocumentoContrato(contratoID uint, generadoPor string) (*models.DocumentoGenerado, error) {
|
||||||
|
contrato, err := models.GetContratoByID(contratoID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("contrato no encontrado: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
datos := DatosBaseDocumento(map[string]interface{}{
|
||||||
|
"Contrato": contrato,
|
||||||
|
"Cliente": contrato.Cliente,
|
||||||
|
"Servicios": contrato.Servicios,
|
||||||
|
"FechaInicio": contrato.FechaInicio.Format("02/01/2006"),
|
||||||
|
"FechaVencimiento": contrato.FechaVencimiento.Format("02/01/2006"),
|
||||||
|
"PrecioAcordado": contrato.PrecioAcordado,
|
||||||
|
"Moneda": contrato.Moneda,
|
||||||
|
"Notas": contrato.Notas,
|
||||||
|
})
|
||||||
|
|
||||||
|
doc, _, err := GenerarDocumento("contrato", datos, &contrato.ClienteID, nil, generadoPor)
|
||||||
|
return doc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrearContratoConDocumento crea el registro de contrato y de una vez genera su PDF
|
||||||
|
// con cláusulas estándar, para el flujo "cliente, tipo de servicio, duración" que
|
||||||
|
// describe la automatización con IA (Telegram / chat propio / Claude directo).
|
||||||
|
func CrearContratoConDocumento(clienteID uint, servicioIDs []uint, duracionMeses int, precioAcordado float64, moneda, notas, generadoPor string) (*models.Contrato, *models.DocumentoGenerado, error) {
|
||||||
|
if clienteID == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("cliente_id requerido")
|
||||||
|
}
|
||||||
|
if len(servicioIDs) == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("servicio_ids requerido (al menos un servicio)")
|
||||||
|
}
|
||||||
|
if duracionMeses <= 0 {
|
||||||
|
duracionMeses = 12
|
||||||
|
}
|
||||||
|
if moneda == "" {
|
||||||
|
moneda = "COP"
|
||||||
|
}
|
||||||
|
|
||||||
|
inicio := time.Now()
|
||||||
|
vencimiento := inicio.AddDate(0, duracionMeses, 0)
|
||||||
|
|
||||||
|
c := models.Contrato{
|
||||||
|
ClienteID: clienteID,
|
||||||
|
FechaInicio: inicio,
|
||||||
|
FechaVencimiento: vencimiento,
|
||||||
|
PrecioAcordado: precioAcordado,
|
||||||
|
Moneda: moneda,
|
||||||
|
Estado: "activo",
|
||||||
|
Notas: notas,
|
||||||
|
}
|
||||||
|
if err := models.CreateContrato(c, servicioIDs, nil, nil, nil); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("no se pudo crear el contrato: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recargar con Cliente/Servicios precargados (CreateContrato no los devuelve).
|
||||||
|
creado, err := models.GetUltimoContratoByCliente(clienteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("contrato creado pero no se pudo recargar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := GenerarDocumentoContrato(creado.ID, generadoPor)
|
||||||
|
if err != nil {
|
||||||
|
return creado, nil, err
|
||||||
|
}
|
||||||
|
return creado, doc, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerarDocumentoCuentaCobro produce el PDF de solicitud de cuenta de cobro para
|
||||||
|
// una cuenta ya existente, usando la plantilla activa tipo 'cuenta_cobro' + los
|
||||||
|
// datos fiscales de U-SITE (DatosBaseDocumento), para que salga correcta sin
|
||||||
|
// revisión manual.
|
||||||
|
func GenerarDocumentoCuentaCobro(cuentaCobroID uint, generadoPor string) (*models.DocumentoGenerado, error) {
|
||||||
|
cc, err := models.GetCuentaCobroByID(cuentaCobroID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cuenta de cobro no encontrada: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
datos := DatosBaseDocumento(map[string]interface{}{
|
||||||
|
"CuentaCobro": cc,
|
||||||
|
"Cliente": cc.Cliente,
|
||||||
|
"Descripcion": cc.Descripcion,
|
||||||
|
"Valor": cc.Valor,
|
||||||
|
"FechaCuenta": cc.Fecha.Format("02/01/2006"),
|
||||||
|
"Notas": cc.Notas,
|
||||||
|
})
|
||||||
|
|
||||||
|
doc, _, err := GenerarDocumento("cuenta_cobro", datos, &cc.ClienteID, nil, generadoPor)
|
||||||
|
return doc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrearCuentaCobroConDocumento crea el registro de cuenta de cobro y de una vez
|
||||||
|
// genera su PDF, para el flujo "proyecto, monto, periodo" de la automatización con IA.
|
||||||
|
func CrearCuentaCobroConDocumento(clienteID uint, descripcion string, valor float64, fechaVencimiento *time.Time, notas, generadoPor string) (*models.CuentaCobro, *models.DocumentoGenerado, error) {
|
||||||
|
if clienteID == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("cliente_id requerido")
|
||||||
|
}
|
||||||
|
if valor <= 0 {
|
||||||
|
return nil, nil, fmt.Errorf("valor debe ser mayor a cero")
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := &models.CuentaCobro{
|
||||||
|
ClienteID: clienteID,
|
||||||
|
Fecha: time.Now(),
|
||||||
|
Descripcion: descripcion,
|
||||||
|
Valor: valor,
|
||||||
|
Estado: "pendiente",
|
||||||
|
FechaVencimiento: fechaVencimiento,
|
||||||
|
Notas: notas,
|
||||||
|
}
|
||||||
|
if err := models.CreateCuentaCobro(cc); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("no se pudo crear la cuenta de cobro: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := GenerarDocumentoCuentaCobro(cc.ID, generadoPor)
|
||||||
|
if err != nil {
|
||||||
|
return cc, nil, err
|
||||||
|
}
|
||||||
|
return cc, doc, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerarDocumento es el punto único que usan cotizaciones, contratos, actas de
|
||||||
|
// proyecto y cuentas de cobro para producir su PDF: toma la plantilla activa del
|
||||||
|
// tipo pedido (Capa 1, fuente de verdad editable en /plantillas-documento), la
|
||||||
|
// renderiza con los datos que arma cada endpoint, la convierte a PDF y la deja
|
||||||
|
// guardada en el historial (DocumentoGenerado) asociada al cliente/proyecto.
|
||||||
|
func GenerarDocumento(tipo string, datos map[string]interface{}, clienteID, proyectoID *uint, generadoPor string) (*models.DocumentoGenerado, []byte, error) {
|
||||||
|
plantilla, err := models.GetPlantillaDocumentoActiva(tipo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("no hay una plantilla activa para el tipo '%s': %w", tipo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("documento").Parse(plantilla.ContenidoHTML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("plantilla HTML inválida: %w", err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, datos); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error al renderizar plantilla: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfBytes, err := RenderHTMLToPDF(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%s-%s.pdf", tipo, uuid.NewString())
|
||||||
|
subdir := tipo
|
||||||
|
if clienteID != nil {
|
||||||
|
subdir = fmt.Sprintf("%s/%d", tipo, *clienteID)
|
||||||
|
}
|
||||||
|
path, err := SaveGeneratedFile(subdir, filename, pdfBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := &models.DocumentoGenerado{
|
||||||
|
Tipo: tipo,
|
||||||
|
ClienteID: clienteID,
|
||||||
|
ProyectoID: proyectoID,
|
||||||
|
PlantillaID: &plantilla.ID,
|
||||||
|
Nombre: filename,
|
||||||
|
Archivo: path,
|
||||||
|
TipoMime: "application/pdf",
|
||||||
|
Tamanio: int64(len(pdfBytes)),
|
||||||
|
DatosJSON: marshalDatos(datos),
|
||||||
|
GeneradoPor: generadoPor,
|
||||||
|
}
|
||||||
|
if err := models.CreateDocumentoGenerado(doc); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("documento generado pero no se pudo guardar en el historial: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc, pdfBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemCotizacion es un renglón de una cotización con el precio ya calculado
|
||||||
|
// por quien llama (Claude, tras consultar /api/v2/tarifas). El backend solo suma.
|
||||||
|
type ItemCotizacion struct {
|
||||||
|
Descripcion string `json:"descripcion"`
|
||||||
|
Cantidad float64 `json:"cantidad"`
|
||||||
|
ValorUnitario float64 `json:"valor_unitario"`
|
||||||
|
Unidad string `json:"unidad"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrearCotizacion es la única implementación de "generar cotización": la usan por
|
||||||
|
// igual el endpoint POST /api/v2/cotizaciones y la tool crear_cotizacion del
|
||||||
|
// agente de Telegram/Claude, para que el comportamiento sea idéntico sin importar el canal.
|
||||||
|
func CrearCotizacion(clienteID uint, alcance, tipoProyecto string, items []ItemCotizacion, generadoPor string) (*models.DocumentoGenerado, float64, error) {
|
||||||
|
if clienteID == 0 {
|
||||||
|
return nil, 0, fmt.Errorf("cliente_id requerido")
|
||||||
|
}
|
||||||
|
if alcance == "" {
|
||||||
|
return nil, 0, fmt.Errorf("alcance requerido")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, 0, fmt.Errorf("items requerido (al menos un ítem con descripcion, cantidad y valor_unitario)")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliente, err := models.GetClienteByID(clienteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("cliente no encontrado: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, it := range items {
|
||||||
|
total += it.Cantidad * it.ValorUnitario
|
||||||
|
}
|
||||||
|
|
||||||
|
datos := DatosBaseDocumento(map[string]interface{}{
|
||||||
|
"Cliente": cliente,
|
||||||
|
"Alcance": alcance,
|
||||||
|
"TipoProyecto": tipoProyecto,
|
||||||
|
"Items": items,
|
||||||
|
"Total": total,
|
||||||
|
})
|
||||||
|
|
||||||
|
doc, _, err := GenerarDocumento("cotizacion", datos, &clienteID, nil, generadoPor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return doc, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatosBaseDocumento agrega los campos comunes a cualquier plantilla (fecha,
|
||||||
|
// datos fiscales de U-SITE) al mapa de datos específicos de cada tipo de documento.
|
||||||
|
func DatosBaseDocumento(extra map[string]interface{}) map[string]interface{} {
|
||||||
|
base := map[string]interface{}{
|
||||||
|
"Fecha": time.Now().Format("02/01/2006"),
|
||||||
|
"EmpresaNombre": "U-SITE S.A.S BIC",
|
||||||
|
"EmpresaWeb": "https://u-site.app",
|
||||||
|
}
|
||||||
|
for k, v := range extra {
|
||||||
|
base[k] = v
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalDatos(datos map[string]interface{}) string {
|
||||||
|
b, err := json.Marshal(datos)
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderHTMLToPDF convierte un documento HTML (ya renderizado, con JS/CSS inline si aplica)
|
||||||
|
// a PDF usando Chrome headless. Es el motor de "impresión" que comparten cotizaciones,
|
||||||
|
// contratos, actas de proyecto y cuentas de cobro: la plantilla se renderiza a HTML con
|
||||||
|
// text/template y este servicio la convierte al PDF final.
|
||||||
|
//
|
||||||
|
// Requiere Chrome/Chromium instalado en el host (ver Dockerfile). La ruta del binario se
|
||||||
|
// puede forzar con la variable de entorno CHROME_EXEC_PATH; si no está definida, chromedp
|
||||||
|
// intenta ubicar un Chrome instalado en el sistema.
|
||||||
|
func RenderHTMLToPDF(html string) ([]byte, error) {
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.Flag("headless", true),
|
||||||
|
chromedp.Flag("no-sandbox", true),
|
||||||
|
chromedp.Flag("disable-gpu", true),
|
||||||
|
chromedp.Flag("disable-dev-shm-usage", true),
|
||||||
|
)
|
||||||
|
if execPath := os.Getenv("CHROME_EXEC_PATH"); execPath != "" {
|
||||||
|
opts = append(opts, chromedp.ExecPath(execPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
|
defer cancelAlloc()
|
||||||
|
|
||||||
|
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancelTimeout()
|
||||||
|
|
||||||
|
dataURI := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(html))
|
||||||
|
|
||||||
|
var pdfBuf []byte
|
||||||
|
err := chromedp.Run(ctx,
|
||||||
|
chromedp.Navigate(dataURI),
|
||||||
|
chromedp.WaitReady("body", chromedp.ByQuery),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
buf, _, err := page.PrintToPDF().
|
||||||
|
WithPrintBackground(true).
|
||||||
|
WithPreferCSSPageSize(true).
|
||||||
|
WithMarginTop(0.4).
|
||||||
|
WithMarginBottom(0.4).
|
||||||
|
WithMarginLeft(0.4).
|
||||||
|
WithMarginRight(0.4).
|
||||||
|
Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pdfBuf = buf
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generando PDF: %w", err)
|
||||||
|
}
|
||||||
|
return pdfBuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveGeneratedFile persiste bytes generados (ej. un PDF) en disco bajo uploads/documentos/{subdir}
|
||||||
|
// y retorna la ruta relativa guardada en el modelo (DocumentoGenerado.Archivo, ProyectoDocumento.Archivo, etc).
|
||||||
|
func SaveGeneratedFile(subdir, filename string, data []byte) (string, error) {
|
||||||
|
dir := fmt.Sprintf("uploads/documentos/%s", subdir)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("no se pudo crear el directorio %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
path := dir + "/" + filename
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("no se pudo escribir %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func slugifyProyecto(s string) string {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
" ", "-", "á", "a", "é", "e", "í", "i", "ó", "o", "ú", "u",
|
||||||
|
"ñ", "n", "ü", "u",
|
||||||
|
)
|
||||||
|
s = replacer.Replace(s)
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Trim(b.String(), "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrearProyectoSimple crea un proyecto para un cliente a partir del flujo de
|
||||||
|
// automatización con IA ("nombre, stack, cliente asociado"). El stack se antepone
|
||||||
|
// a la descripción; la creación de repo/estructura inicial queda fuera de este
|
||||||
|
// alcance (requiere integración con un proveedor Git, no cubierta aún).
|
||||||
|
func CrearProyectoSimple(clienteID uint, nombre, stack, descripcion string) (*models.Proyecto, error) {
|
||||||
|
if clienteID == 0 {
|
||||||
|
return nil, fmt.Errorf("cliente_id requerido")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(nombre) == "" {
|
||||||
|
return nil, fmt.Errorf("nombre requerido")
|
||||||
|
}
|
||||||
|
if stack != "" {
|
||||||
|
descripcion = fmt.Sprintf("Stack: %s\n\n%s", stack, descripcion)
|
||||||
|
}
|
||||||
|
p := &models.Proyecto{
|
||||||
|
ClienteID: clienteID,
|
||||||
|
Nombre: nombre,
|
||||||
|
Slug: slugifyProyecto(nombre),
|
||||||
|
Descripcion: descripcion,
|
||||||
|
Color: "#8eb02f",
|
||||||
|
Estado: "activo",
|
||||||
|
}
|
||||||
|
if err := models.CreateProyecto(p); err != nil {
|
||||||
|
return nil, fmt.Errorf("no se pudo crear el proyecto: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
@@ -165,6 +165,17 @@ func agentTools() []agentTool {
|
|||||||
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
||||||
tool("enviar_correo_contrato", "Envía notificación de renovación por correo al cliente.",
|
tool("enviar_correo_contrato", "Envía notificación de renovación por correo al cliente.",
|
||||||
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
||||||
|
tool("crear_contrato", "Crea un contrato para un cliente con uno o más servicios y genera de una vez su PDF con cláusulas estándar.",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
||||||
|
"servicio_ids": agentToolParam{Type: "array", Description: "IDs de los servicios contratados", Items: &agentToolParam{Type: "number"}},
|
||||||
|
"duracion_meses": num("Duración del contrato en meses (default 12)"),
|
||||||
|
"precio_acordado": num("Valor total acordado"),
|
||||||
|
"moneda": str("Moneda, ej: COP, USD (default COP)"),
|
||||||
|
"notas": str("Notas adicionales del contrato"),
|
||||||
|
}, []string{"cliente_id", "servicio_ids", "precio_acordado"})),
|
||||||
|
tool("generar_documento_contrato", "Genera (o regenera) el PDF de un contrato ya existente a partir de la plantilla activa.",
|
||||||
|
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
|
||||||
|
|
||||||
// ── Contabilidad ─────────────────────────────────────────────────────
|
// ── Contabilidad ─────────────────────────────────────────────────────
|
||||||
tool("dashboard_contabilidad", "Resumen del mes: ingresos, egresos, pendientes.",
|
tool("dashboard_contabilidad", "Resumen del mes: ingresos, egresos, pendientes.",
|
||||||
@@ -192,18 +203,72 @@ func agentTools() []agentTool {
|
|||||||
"page": num("Página"),
|
"page": num("Página"),
|
||||||
"estado": str("pendiente | pagada | vencida"),
|
"estado": str("pendiente | pagada | vencida"),
|
||||||
}, nil)),
|
}, nil)),
|
||||||
|
tool("crear_cuenta_cobro", "Crea una cuenta de cobro para un cliente/proyecto y genera de una vez su PDF de solicitud.",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"cliente_id": num("ID del cliente"),
|
||||||
|
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
|
||||||
|
"valor": num("Valor a cobrar"),
|
||||||
|
"notas": str("Notas adicionales"),
|
||||||
|
}, []string{"cliente_id", "descripcion", "valor"})),
|
||||||
|
tool("generar_documento_cuenta_cobro", "Genera (o regenera) el PDF de una cuenta de cobro ya existente.",
|
||||||
|
obj(map[string]agentToolParam{"id": num("ID de la cuenta de cobro")}, []string{"id"})),
|
||||||
tool("listar_cuentas_pagar", "Lista cuentas por pagar (lo que debemos).",
|
tool("listar_cuentas_pagar", "Lista cuentas por pagar (lo que debemos).",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"page": num("Página"),
|
"page": num("Página"),
|
||||||
"estado": str("pendiente | pagada | vencida"),
|
"estado": str("pendiente | pagada | vencida"),
|
||||||
}, nil)),
|
}, nil)),
|
||||||
|
|
||||||
|
// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
|
||||||
|
tool("listar_tarifas", "Lista las tarifas activas (valor por hora, licencias, VMs, márgenes) para armar cotizaciones.",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"categoria": str("Filtrar por categoría: hora_servicio | licencia | vm_azure | margen | otro"),
|
||||||
|
}, nil)),
|
||||||
|
tool("crear_cotizacion", "Genera el PDF de una cotización para un cliente, a partir de la plantilla activa y los items con precio ya calculado (consulta listar_tarifas antes para saber los valores).",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
||||||
|
"alcance": str("Descripción del alcance del proyecto o servicio a cotizar"),
|
||||||
|
"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
|
||||||
|
"items": agentToolParam{
|
||||||
|
Type: "array",
|
||||||
|
Description: "Ítems de la cotización con precio ya calculado",
|
||||||
|
Items: &agentToolParam{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]agentToolParam{
|
||||||
|
"descripcion": str("Descripción del ítem"),
|
||||||
|
"cantidad": num("Cantidad"),
|
||||||
|
"valor_unitario": num("Valor unitario en COP"),
|
||||||
|
"unidad": str("hora | mes | unico"),
|
||||||
|
},
|
||||||
|
Required: []string{"descripcion", "cantidad", "valor_unitario"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, []string{"cliente_id", "alcance", "items"})),
|
||||||
|
|
||||||
|
// ── Automatización IA: arquitecturas ────────────────────────────────────
|
||||||
|
tool("listar_arquitecturas_referencia", "Lista los patrones de arquitectura técnica ya resueltos (ej: AD redundante en Azure con VPN Gateway) para reutilizarlos en vez de improvisar.",
|
||||||
|
obj(map[string]agentToolParam{"search": str("Búsqueda por nombre o tag")}, nil)),
|
||||||
|
tool("generar_arquitectura", "Genera el PDF de una propuesta técnica ya redactada (consulta antes listar_arquitecturas_referencia para reutilizar patrones existentes).",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"requerimiento": str("Requerimiento del cliente en texto libre"),
|
||||||
|
"propuesta": str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
|
||||||
|
"nombre": str("Título de la propuesta"),
|
||||||
|
"cliente_id": num("ID del cliente (opcional)"),
|
||||||
|
"guardar_como_referencia": agentToolParam{Type: "boolean", Description: "Si true, guarda esta propuesta como patrón reutilizable para el futuro"},
|
||||||
|
}, []string{"requerimiento", "propuesta"})),
|
||||||
|
|
||||||
// ── Proyectos ────────────────────────────────────────────────────────
|
// ── Proyectos ────────────────────────────────────────────────────────
|
||||||
tool("listar_proyectos", "Lista proyectos.",
|
tool("listar_proyectos", "Lista proyectos.",
|
||||||
obj(map[string]agentToolParam{
|
obj(map[string]agentToolParam{
|
||||||
"page": num("Página"),
|
"page": num("Página"),
|
||||||
"search": str("Búsqueda"),
|
"search": str("Búsqueda"),
|
||||||
}, nil)),
|
}, nil)),
|
||||||
|
tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
|
||||||
|
obj(map[string]agentToolParam{
|
||||||
|
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
|
||||||
|
"nombre": str("Nombre del proyecto"),
|
||||||
|
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||||
|
"descripcion": str("Descripción adicional del proyecto"),
|
||||||
|
}, []string{"cliente_id", "nombre"})),
|
||||||
|
|
||||||
// ── Tickets ──────────────────────────────────────────────────────────
|
// ── Tickets ──────────────────────────────────────────────────────────
|
||||||
tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
|
tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
|
||||||
@@ -294,9 +359,9 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
names[i] = fmt.Sprintf("#%d %s (%s)", c.ID, c.Nombre, c.BaseURL)
|
names[i] = fmt.Sprintf("#%d %s (%s)", c.ID, c.Nombre, c.BaseURL)
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"sistema": "U-Site Admin",
|
"sistema": "U-Site Admin",
|
||||||
"coolify_instancias": names,
|
"coolify_instancias": names,
|
||||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
// ── Coolify ────────────────────────────────────────────────────────────
|
// ── Coolify ────────────────────────────────────────────────────────────
|
||||||
@@ -328,9 +393,9 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
return nil, fmt.Errorf("instancia Coolify #%d no encontrada", configID)
|
return nil, fmt.Errorf("instancia Coolify #%d no encontrada", configID)
|
||||||
}
|
}
|
||||||
endpoint := map[string]string{
|
endpoint := map[string]string{
|
||||||
"coolify_apps": "/applications",
|
"coolify_apps": "/applications",
|
||||||
"coolify_servicios": "/services",
|
"coolify_servicios": "/services",
|
||||||
"coolify_servidores": "/servers",
|
"coolify_servidores": "/servers",
|
||||||
}[name]
|
}[name]
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
uuid := getStr("uuid")
|
uuid := getStr("uuid")
|
||||||
@@ -464,6 +529,51 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
"cliente_id": contrato.ClienteID,
|
"cliente_id": contrato.ClienteID,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case "crear_contrato":
|
||||||
|
idsRaw, _ := a["servicio_ids"].([]interface{})
|
||||||
|
servicioIDs := make([]uint, 0, len(idsRaw))
|
||||||
|
for _, v := range idsRaw {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
servicioIDs = append(servicioIDs, uint(x))
|
||||||
|
case int:
|
||||||
|
servicioIDs = append(servicioIDs, uint(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contrato, doc, err := CrearContratoConDocumento(
|
||||||
|
uint(getInt("cliente_id", 0)),
|
||||||
|
servicioIDs,
|
||||||
|
getInt("duracion_meses", 12),
|
||||||
|
float64(getInt("precio_acordado", 0)),
|
||||||
|
getStr("moneda"),
|
||||||
|
getStr("notas"),
|
||||||
|
"telegram",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{"ok": true, "contrato_id": contrato.ID}
|
||||||
|
if doc != nil {
|
||||||
|
result["documento_id"] = doc.ID
|
||||||
|
result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case "generar_documento_contrato":
|
||||||
|
id := uint(getInt("id", 0))
|
||||||
|
if id == 0 {
|
||||||
|
return nil, fmt.Errorf("id requerido")
|
||||||
|
}
|
||||||
|
doc, err := GenerarDocumentoContrato(id, "telegram")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"documento_id": doc.ID,
|
||||||
|
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
|
||||||
|
}, nil
|
||||||
|
|
||||||
// ── Contabilidad ───────────────────────────────────────────────────────
|
// ── Contabilidad ───────────────────────────────────────────────────────
|
||||||
case "dashboard_contabilidad":
|
case "dashboard_contabilidad":
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -511,6 +621,40 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
|
case "crear_cuenta_cobro":
|
||||||
|
cc, doc, err := CrearCuentaCobroConDocumento(
|
||||||
|
uint(getInt("cliente_id", 0)),
|
||||||
|
getStr("descripcion"),
|
||||||
|
float64(getInt("valor", 0)),
|
||||||
|
nil,
|
||||||
|
getStr("notas"),
|
||||||
|
"telegram",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{"ok": true, "cuenta_cobro_id": cc.ID}
|
||||||
|
if doc != nil {
|
||||||
|
result["documento_id"] = doc.ID
|
||||||
|
result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case "generar_documento_cuenta_cobro":
|
||||||
|
id := uint(getInt("id", 0))
|
||||||
|
if id == 0 {
|
||||||
|
return nil, fmt.Errorf("id requerido")
|
||||||
|
}
|
||||||
|
doc, err := GenerarDocumentoCuentaCobro(id, "telegram")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"documento_id": doc.ID,
|
||||||
|
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
|
||||||
|
}, nil
|
||||||
|
|
||||||
case "listar_cuentas_pagar":
|
case "listar_cuentas_pagar":
|
||||||
page := getInt("page", 1)
|
page := getInt("page", 1)
|
||||||
estado := getStr("estado")
|
estado := getStr("estado")
|
||||||
@@ -522,6 +666,77 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
|
// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
|
||||||
|
case "listar_tarifas":
|
||||||
|
items, err := models.GetTarifasActivas(getStr("categoria"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
|
||||||
|
case "crear_cotizacion":
|
||||||
|
itemsRaw, _ := a["items"].([]interface{})
|
||||||
|
items := make([]ItemCotizacion, 0, len(itemsRaw))
|
||||||
|
for _, raw := range itemsRaw {
|
||||||
|
m, ok := raw.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toFloat := func(v interface{}) float64 {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return x
|
||||||
|
case int:
|
||||||
|
return float64(x)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
toStr := func(v interface{}) string {
|
||||||
|
s, _ := v.(string)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
items = append(items, ItemCotizacion{
|
||||||
|
Descripcion: toStr(m["descripcion"]),
|
||||||
|
Cantidad: toFloat(m["cantidad"]),
|
||||||
|
ValorUnitario: toFloat(m["valor_unitario"]),
|
||||||
|
Unidad: toStr(m["unidad"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
doc, total, err := CrearCotizacion(uint(getInt("cliente_id", 0)), getStr("alcance"), getStr("tipo_proyecto"), items, "telegram")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"documento_id": doc.ID,
|
||||||
|
"total": total,
|
||||||
|
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
// ── Automatización IA: arquitecturas ────────────────────────────────────
|
||||||
|
case "listar_arquitecturas_referencia":
|
||||||
|
items, _, err := models.GetAllArquitecturas(50, 0, getStr("search"), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
|
||||||
|
case "generar_arquitectura":
|
||||||
|
var clienteID *uint
|
||||||
|
if id := getInt("cliente_id", 0); id > 0 {
|
||||||
|
u := uint(id)
|
||||||
|
clienteID = &u
|
||||||
|
}
|
||||||
|
doc, err := GenerarArquitectura(getStr("requerimiento"), getStr("propuesta"), getStr("nombre"), clienteID, getBool("guardar_como_referencia"), "telegram")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"documento_id": doc.ID,
|
||||||
|
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
|
||||||
|
}, nil
|
||||||
|
|
||||||
// ── Proyectos ──────────────────────────────────────────────────────────
|
// ── Proyectos ──────────────────────────────────────────────────────────
|
||||||
case "listar_proyectos":
|
case "listar_proyectos":
|
||||||
page := getInt("page", 1)
|
page := getInt("page", 1)
|
||||||
@@ -534,6 +749,13 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||||
|
|
||||||
|
case "crear_proyecto":
|
||||||
|
p, err := CrearProyectoSimple(uint(getInt("cliente_id", 0)), getStr("nombre"), getStr("stack"), getStr("descripcion"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]interface{}{"ok": true, "id": p.ID, "slug": p.Slug}, nil
|
||||||
|
|
||||||
// ── Tickets ────────────────────────────────────────────────────────────
|
// ── Tickets ────────────────────────────────────────────────────────────
|
||||||
case "listar_tickets":
|
case "listar_tickets":
|
||||||
estado := getStr("estado")
|
estado := getStr("estado")
|
||||||
@@ -699,10 +921,10 @@ type anthropicMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type anthropicReq struct {
|
type anthropicReq struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens"`
|
||||||
System string `json:"system,omitempty"`
|
System string `json:"system,omitempty"`
|
||||||
Messages []anthropicMsg `json:"messages"`
|
Messages []anthropicMsg `json:"messages"`
|
||||||
Tools []anthropicTool `json:"tools,omitempty"`
|
Tools []anthropicTool `json:"tools,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<div x-data="app" x-init="init" class="bg-white rounded-lg shadow flex flex-col" style="height: calc(100vh - 140px)">
|
||||||
|
<div class="border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold mb-1">Asistente U-Site</h1>
|
||||||
|
<p class="text-sm text-gray-500">Mismo motor y mismas herramientas que el bot de Telegram: cotizaciones, contratos, contabilidad, proyectos, Coolify…</p>
|
||||||
|
</div>
|
||||||
|
<button @click="reset()" class="text-sm text-gray-400 hover:text-red-500 border rounded px-3 py-1.5">Borrar historial</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto px-6 py-4 space-y-4" x-ref="scrollArea">
|
||||||
|
<template x-if="mensajes.length === 0">
|
||||||
|
<div class="text-center text-gray-400 text-sm mt-10">
|
||||||
|
Escríbeme en lenguaje natural, por ejemplo:<br>
|
||||||
|
<em>"cotización para ZABDI, migración a M365 Business Premium para 25 usuarios"</em>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-for="(m, i) in mensajes" :key="i">
|
||||||
|
<div class="flex" :class="m.rol === 'user' ? 'justify-end' : 'justify-start'">
|
||||||
|
<div class="max-w-[75%] rounded-lg px-4 py-2 text-sm whitespace-pre-wrap"
|
||||||
|
:class="m.rol === 'user' ? 'bg-[#8eb02f] text-white' : 'bg-gray-100 text-gray-800'"
|
||||||
|
x-text="m.texto"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="pensando" class="flex justify-start">
|
||||||
|
<div class="bg-gray-100 text-gray-400 rounded-lg px-4 py-2 text-sm">Pensando…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="enviar()" class="border-t border-gray-200 p-4 flex gap-2">
|
||||||
|
<input x-model="input" :disabled="pensando" placeholder="Escribe tu mensaje…"
|
||||||
|
class="flex-1 border rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#8eb02f]/40" />
|
||||||
|
<button type="submit" :disabled="pensando || !input.trim()" class="bg-[#8eb02f] text-white px-5 py-2 rounded-lg text-sm disabled:opacity-40">
|
||||||
|
Enviar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('app', () => ({
|
||||||
|
mensajes: [],
|
||||||
|
input: '',
|
||||||
|
pensando: false,
|
||||||
|
|
||||||
|
init() {},
|
||||||
|
|
||||||
|
scrollDown() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const el = this.$refs.scrollArea;
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async enviar() {
|
||||||
|
const texto = this.input.trim();
|
||||||
|
if (!texto || this.pensando) return;
|
||||||
|
this.mensajes.push({ rol: 'user', texto });
|
||||||
|
this.input = '';
|
||||||
|
this.pensando = true;
|
||||||
|
this.scrollDown();
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post('/app/api/asistente/chat', { mensaje: texto });
|
||||||
|
this.mensajes.push({ rol: 'assistant', texto: data.respuesta || '' });
|
||||||
|
} catch (e) {
|
||||||
|
this.mensajes.push({ rol: 'assistant', texto: e.response?.data?.error || 'Error al contactar el asistente.' });
|
||||||
|
}
|
||||||
|
this.pensando = false;
|
||||||
|
this.scrollDown();
|
||||||
|
},
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
if (!confirm('¿Borrar el historial de esta conversación?')) return;
|
||||||
|
try {
|
||||||
|
await axios.delete('/app/api/asistente/historial');
|
||||||
|
this.mensajes = [];
|
||||||
|
} catch (e) { /* noop */ }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
<div x-data="app" class="bg-white rounded-lg shadow">
|
||||||
|
<div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
|
||||||
|
<img src="../img/loading.gif" alt="Cargando..." class="w-16 h-16" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-6 w-full">
|
||||||
|
<div class="justify-between items-center w-full md:flex mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold mb-1">Plantillas de Documento</h1>
|
||||||
|
<p class="text-sm text-gray-500">Fuente de verdad de cotizaciones, contratos, actas y cuentas de cobro generadas por IA</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-3 md:mt-0">
|
||||||
|
<select x-model="filtroTipo" @change="loadData()" class="border border-gray-300 rounded p-2 text-sm">
|
||||||
|
<option value="">Todos los tipos</option>
|
||||||
|
<option value="cotizacion">Cotización</option>
|
||||||
|
<option value="contrato">Contrato</option>
|
||||||
|
<option value="acta">Acta</option>
|
||||||
|
<option value="cuenta_cobro">Cuenta de cobro</option>
|
||||||
|
</select>
|
||||||
|
<button @click="addModal = true" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nueva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table-auto w-full text-sm">
|
||||||
|
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 px-3">Nombre</th>
|
||||||
|
<th class="py-2 px-3">Tipo</th>
|
||||||
|
<th class="py-2 px-3">Versión</th>
|
||||||
|
<th class="py-2 px-3">Estado</th>
|
||||||
|
<th class="py-2 px-3 w-24"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-gray-600">
|
||||||
|
<template x-for="d in datos" :key="d.ID">
|
||||||
|
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||||
|
<td class="py-2 px-3 font-medium" x-text="d.nombre"></td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span class="px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-700" x-text="d.tipo"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3" x-text="'v'+d.version"></td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span class="px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
:class="d.activa ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
|
||||||
|
x-text="d.activa ? 'Activa' : 'Inactiva'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button @click="openEdit(d)" title="Editar" class="text-gray-400 hover:text-[#8eb02f]">
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="openDelete(d)" title="Eliminar" class="text-gray-400 hover:text-red-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr x-show="!loading && datos.length===0">
|
||||||
|
<td colspan="5" class="text-center text-gray-400 py-8">Sin plantillas</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4 text-sm text-gray-500">
|
||||||
|
<span>Total: <span x-text="total"></span></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="prevPage()" :disabled="page===1" class="px-3 py-1 rounded border disabled:opacity-40">‹</button>
|
||||||
|
<span class="px-3 py-1" x-text="'Pág. '+page+' / '+totalPages"></span>
|
||||||
|
<button @click="nextPage()" :disabled="page>=totalPages" class="px-3 py-1 rounded border disabled:opacity-40">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Crear / Editar -->
|
||||||
|
<div x-show="addModal || editModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 p-6 max-h-[90vh] overflow-y-auto" @click.stop>
|
||||||
|
<h2 class="text-lg font-semibold mb-4" x-text="editModal ? 'Editar Plantilla' : 'Nueva Plantilla'"></h2>
|
||||||
|
|
||||||
|
<div class="mb-4 p-3 bg-blue-50 rounded text-xs text-blue-700 leading-6">
|
||||||
|
<strong>Variables comunes:</strong>
|
||||||
|
<code>{{.Cliente}}</code>,
|
||||||
|
<code>{{.Alcance}}</code>,
|
||||||
|
<code>{{.Fecha}}</code>,
|
||||||
|
<code>{{.EmpresaNombre}}</code>,
|
||||||
|
<code>{{.Total}}</code>,
|
||||||
|
<code>{{range .Items}}...{{.Descripcion}}...{{end}}</code>
|
||||||
|
<span class="block mt-1 text-blue-500">Cada tipo de documento pasa campos adicionales propios (ver /api/v2/spec).</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-3 gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Nombre *</label>
|
||||||
|
<input x-model="form.nombre" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Tipo</label>
|
||||||
|
<select x-model="form.tipo" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
|
<option value="cotizacion">Cotización</option>
|
||||||
|
<option value="contrato">Contrato</option>
|
||||||
|
<option value="acta">Acta</option>
|
||||||
|
<option value="cuenta_cobro">Cuenta de cobro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end gap-2">
|
||||||
|
<label class="flex items-center gap-2 text-sm mb-2">
|
||||||
|
<input type="checkbox" x-model="form.activa" /> Activa
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600 block mb-1">HTML de la plantilla (Go text/template) *</label>
|
||||||
|
<textarea x-model="form.contenido_html" rows="16"
|
||||||
|
@input.debounce.600ms="validar()"
|
||||||
|
:class="templateError ? 'border-red-400' : ''"
|
||||||
|
class="w-full border rounded px-3 py-2 text-xs font-mono" required></textarea>
|
||||||
|
<p x-show="templateError" x-text="templateError" class="text-red-500 text-xs mt-1"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 mt-5">
|
||||||
|
<button type="button" @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-[#8eb02f] text-white rounded text-sm" :disabled="loading">
|
||||||
|
<span x-text="loading ? 'Guardando…' : 'Guardar'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Eliminar -->
|
||||||
|
<div x-show="deleteModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-sm mx-4 text-center" @click.stop>
|
||||||
|
<p class="font-semibold mb-1">¿Eliminar plantilla?</p>
|
||||||
|
<p class="text-sm text-gray-400 mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<button @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||||
|
<button @click="deleteItem()" class="px-4 py-2 bg-red-500 text-white rounded text-sm">Eliminar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="toast.show" x-cloak x-transition
|
||||||
|
class="fixed bottom-4 right-4 z-[100] px-4 py-3 rounded shadow-lg text-sm text-white"
|
||||||
|
:class="toast.type==='error' ? 'bg-red-500' : 'bg-[#8eb02f]'"
|
||||||
|
x-text="toast.msg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('app', () => ({
|
||||||
|
loading: false,
|
||||||
|
datos: [], total: 0, totalPages: 1, page: 1, limit: 20, filtroTipo: '',
|
||||||
|
addModal: false, editModal: false, deleteModal: false,
|
||||||
|
selectedId: null, templateError: '',
|
||||||
|
form: { nombre:'', tipo:'cotizacion', contenido_html:'', version:1, activa:true },
|
||||||
|
toast: { show:false, msg:'', type:'ok' },
|
||||||
|
|
||||||
|
async init() { await this.loadData(); },
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
const { data } = await axios.get(`/app/api/plantillas-documento?page=${this.page}&limit=${this.limit}&tipo=${this.filtroTipo}`);
|
||||||
|
this.datos = data.registros || [];
|
||||||
|
this.total = data.total || 0;
|
||||||
|
this.totalPages = data.totalPages || 1;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
openEdit(d) {
|
||||||
|
this.form = { nombre: d.nombre, tipo: d.tipo, contenido_html: d.contenido_html, version: d.version, activa: d.activa };
|
||||||
|
this.selectedId = d.ID;
|
||||||
|
this.editModal = true;
|
||||||
|
},
|
||||||
|
openDelete(d) { this.selectedId = d.ID; this.deleteModal = true; },
|
||||||
|
|
||||||
|
async validar() {
|
||||||
|
this.templateError = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModals() {
|
||||||
|
this.addModal = this.editModal = this.deleteModal = false;
|
||||||
|
this.selectedId = null;
|
||||||
|
this.templateError = '';
|
||||||
|
this.form = { nombre:'', tipo:'cotizacion', contenido_html:'', version:1, activa:true };
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
if (this.editModal) {
|
||||||
|
await axios.put(`/app/api/plantillas-documento/${this.selectedId}`, this.form);
|
||||||
|
} else {
|
||||||
|
await axios.post('/app/api/plantillas-documento', this.form);
|
||||||
|
}
|
||||||
|
this.showToast('Guardado');
|
||||||
|
this.closeModals();
|
||||||
|
await this.loadData();
|
||||||
|
} catch(e) {
|
||||||
|
this.templateError = e.response?.data?.error || '';
|
||||||
|
this.showToast(e.response?.data?.error||'Error', 'error');
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
await axios.delete(`/app/api/plantillas-documento/${this.selectedId}`);
|
||||||
|
this.showToast('Eliminado');
|
||||||
|
this.closeModals();
|
||||||
|
await this.loadData();
|
||||||
|
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() { if(this.page>1){ this.page--; this.loadData(); } },
|
||||||
|
nextPage() { if(this.page<this.totalPages){ this.page++; this.loadData(); } },
|
||||||
|
showToast(msg, type='ok') {
|
||||||
|
this.toast = { show:true, msg, type };
|
||||||
|
setTimeout(() => this.toast.show = false, 3000);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<div x-data="app" class="bg-white rounded-lg shadow">
|
||||||
|
<div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
|
||||||
|
<img src="../img/loading.gif" alt="Cargando..." class="w-16 h-16" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-6 w-full">
|
||||||
|
<div class="justify-between items-center w-full md:flex mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold mb-1">Tarifas</h1>
|
||||||
|
<p class="text-sm text-gray-500">Valor por hora, licencias, VMs y márgenes usados por la IA al cotizar</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-3 md:mt-0">
|
||||||
|
<input type="text" placeholder="Buscar..." class="border border-gray-300 rounded p-2 text-sm"
|
||||||
|
x-model="search" @input.debounce.400ms="loadData()" />
|
||||||
|
<button @click="addModal = true" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nueva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table-auto w-full text-sm">
|
||||||
|
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 px-3">Nombre</th>
|
||||||
|
<th class="py-2 px-3">Categoría</th>
|
||||||
|
<th class="py-2 px-3">Valor</th>
|
||||||
|
<th class="py-2 px-3">Unidad</th>
|
||||||
|
<th class="py-2 px-3">Estado</th>
|
||||||
|
<th class="py-2 px-3 w-24"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-gray-600">
|
||||||
|
<template x-for="d in datos" :key="d.ID">
|
||||||
|
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||||
|
<td class="py-2 px-3 font-medium" x-text="d.nombre"></td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span class="px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-700" x-text="d.categoria"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3" x-text="new Intl.NumberFormat('es-CO').format(d.valor) + ' ' + d.moneda"></td>
|
||||||
|
<td class="py-2 px-3" x-text="d.unidad"></td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span class="px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
:class="d.activo ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
|
||||||
|
x-text="d.activo ? 'Activa' : 'Inactiva'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button @click="openEdit(d)" title="Editar" class="text-gray-400 hover:text-[#8eb02f]">
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="openDelete(d)" title="Eliminar" class="text-gray-400 hover:text-red-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr x-show="!loading && datos.length===0">
|
||||||
|
<td colspan="6" class="text-center text-gray-400 py-8">Sin tarifas</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4 text-sm text-gray-500">
|
||||||
|
<span>Total: <span x-text="total"></span></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="prevPage()" :disabled="page===1" class="px-3 py-1 rounded border disabled:opacity-40">‹</button>
|
||||||
|
<span class="px-3 py-1" x-text="'Pág. '+page+' / '+totalPages"></span>
|
||||||
|
<button @click="nextPage()" :disabled="page>=totalPages" class="px-3 py-1 rounded border disabled:opacity-40">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Crear / Editar -->
|
||||||
|
<div x-show="addModal || editModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 p-6" @click.stop>
|
||||||
|
<h2 class="text-lg font-semibold mb-4" x-text="editModal ? 'Editar Tarifa' : 'Nueva Tarifa'"></h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-xs font-medium text-gray-600">Nombre *</label>
|
||||||
|
<input x-model="form.nombre" required class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="ej: M365 Business Premium" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Categoría</label>
|
||||||
|
<select x-model="form.categoria" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
|
<option value="hora_servicio">Hora de servicio</option>
|
||||||
|
<option value="licencia">Licencia</option>
|
||||||
|
<option value="vm_azure">VM Azure</option>
|
||||||
|
<option value="margen">Margen</option>
|
||||||
|
<option value="otro">Otro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Unidad</label>
|
||||||
|
<select x-model="form.unidad" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||||
|
<option value="hora">Hora</option>
|
||||||
|
<option value="mes">Mes</option>
|
||||||
|
<option value="unico">Único</option>
|
||||||
|
<option value="porcentaje">Porcentaje</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Valor *</label>
|
||||||
|
<input x-model.number="form.valor" type="number" step="0.01" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-medium text-gray-600">Moneda</label>
|
||||||
|
<input x-model="form.moneda" class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="COP" />
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="text-xs font-medium text-gray-600">Notas</label>
|
||||||
|
<textarea x-model="form.notas" rows="2" class="mt-1 w-full border rounded px-3 py-2 text-sm"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" x-model="form.activo" /> Activa
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 mt-5">
|
||||||
|
<button type="button" @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-[#8eb02f] text-white rounded text-sm" :disabled="loading">
|
||||||
|
<span x-text="loading ? 'Guardando…' : 'Guardar'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Eliminar -->
|
||||||
|
<div x-show="deleteModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-sm mx-4 text-center" @click.stop>
|
||||||
|
<p class="font-semibold mb-1">¿Eliminar tarifa?</p>
|
||||||
|
<p class="text-sm text-gray-400 mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<button @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||||
|
<button @click="deleteItem()" class="px-4 py-2 bg-red-500 text-white rounded text-sm">Eliminar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="toast.show" x-cloak x-transition
|
||||||
|
class="fixed bottom-4 right-4 z-[100] px-4 py-3 rounded shadow-lg text-sm text-white"
|
||||||
|
:class="toast.type==='error' ? 'bg-red-500' : 'bg-[#8eb02f]'"
|
||||||
|
x-text="toast.msg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('app', () => ({
|
||||||
|
loading: false,
|
||||||
|
datos: [], total: 0, totalPages: 1, page: 1, limit: 50, search: '',
|
||||||
|
addModal: false, editModal: false, deleteModal: false,
|
||||||
|
selectedId: null,
|
||||||
|
form: { nombre:'', categoria:'hora_servicio', valor:0, moneda:'COP', unidad:'hora', notas:'', activo:true },
|
||||||
|
toast: { show:false, msg:'', type:'ok' },
|
||||||
|
|
||||||
|
async init() { await this.loadData(); },
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
const { data } = await axios.get(`/app/api/tarifas?page=${this.page}&limit=${this.limit}&search=${this.search}`);
|
||||||
|
this.datos = data.registros || [];
|
||||||
|
this.total = data.total || 0;
|
||||||
|
this.totalPages = data.totalPages || 1;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
openEdit(d) {
|
||||||
|
this.form = { nombre: d.nombre, categoria: d.categoria, valor: d.valor, moneda: d.moneda, unidad: d.unidad, notas: d.notas, activo: d.activo };
|
||||||
|
this.selectedId = d.ID;
|
||||||
|
this.editModal = true;
|
||||||
|
},
|
||||||
|
openDelete(d) { this.selectedId = d.ID; this.deleteModal = true; },
|
||||||
|
|
||||||
|
closeModals() {
|
||||||
|
this.addModal = this.editModal = this.deleteModal = false;
|
||||||
|
this.selectedId = null;
|
||||||
|
this.form = { nombre:'', categoria:'hora_servicio', valor:0, moneda:'COP', unidad:'hora', notas:'', activo:true };
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
if (this.editModal) {
|
||||||
|
await axios.put(`/app/api/tarifas/${this.selectedId}`, this.form);
|
||||||
|
} else {
|
||||||
|
await axios.post('/app/api/tarifas', this.form);
|
||||||
|
}
|
||||||
|
this.showToast('Guardado');
|
||||||
|
this.closeModals();
|
||||||
|
await this.loadData();
|
||||||
|
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
await axios.delete(`/app/api/tarifas/${this.selectedId}`);
|
||||||
|
this.showToast('Eliminado');
|
||||||
|
this.closeModals();
|
||||||
|
await this.loadData();
|
||||||
|
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() { if(this.page>1){ this.page--; this.loadData(); } },
|
||||||
|
nextPage() { if(this.page<this.totalPages){ this.page++; this.loadData(); } },
|
||||||
|
showToast(msg, type='ok') {
|
||||||
|
this.toast = { show:true, msg, type };
|
||||||
|
setTimeout(() => this.toast.show = false, 3000);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetArquitecturas lista los patrones de arquitectura de referencia.
|
||||||
|
func GetArquitecturas(c *fiber.Ctx) error {
|
||||||
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
records, total, err := models.GetAllArquitecturas(limit, offset, c.Query("search"), c.Query("solo_referencias") == "true")
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"registros": records,
|
||||||
|
"total": total,
|
||||||
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type crearArquitecturaRequest struct {
|
||||||
|
Requerimiento string `json:"requerimiento"`
|
||||||
|
Propuesta string `json:"propuesta"`
|
||||||
|
Nombre string `json:"nombre"`
|
||||||
|
ClienteID uint `json:"cliente_id"`
|
||||||
|
GuardarComoReferencia bool `json:"guardar_como_referencia"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateArquitectura genera el PDF de una propuesta técnica.
|
||||||
|
// POST /api/v2/arquitecturas
|
||||||
|
func CreateArquitectura(c *fiber.Ctx) error {
|
||||||
|
var req crearArquitecturaRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
var clienteID *uint
|
||||||
|
if req.ClienteID != 0 {
|
||||||
|
clienteID = &req.ClienteID
|
||||||
|
}
|
||||||
|
doc, err := services.GenerarArquitectura(req.Requerimiento, req.Propuesta, req.Nombre, clienteID, req.GuardarComoReferencia, generadoPorFromContext(c))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AsistenteView renderiza el chat propio dentro del dashboard: mismo motor y
|
||||||
|
// mismas tools que el bot de Telegram (services.ProcessAgentMessage), solo que
|
||||||
|
// accesible directamente desde la web sin pasar por Telegram.
|
||||||
|
func AsistenteView(c *fiber.Ctx) error {
|
||||||
|
return c.Render("automatizacion/asistente", fiber.Map{
|
||||||
|
"user": c.Locals("user"),
|
||||||
|
"modules": c.Locals("userModules"),
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
|
||||||
|
type asistenteChatRequest struct {
|
||||||
|
Mensaje string `json:"mensaje"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// asistenteChatID deriva un chat_id sintético y estable por usuario del dashboard,
|
||||||
|
// en un rango que nunca colisiona con IDs reales de chats de Telegram (positivos
|
||||||
|
// < 10^10 para chats privados, negativos para grupos/canales).
|
||||||
|
func asistenteChatID(c *fiber.Ctx) int64 {
|
||||||
|
user, _ := c.Locals("user").(map[string]interface{})
|
||||||
|
var userID uint
|
||||||
|
if v, ok := user["ID"]; ok {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case uint:
|
||||||
|
userID = x
|
||||||
|
case int:
|
||||||
|
userID = uint(x)
|
||||||
|
case float64:
|
||||||
|
userID = uint(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 9_000_000_000 + int64(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostAsistenteChat procesa un mensaje del chat propio del dashboard.
|
||||||
|
// POST /app/api/asistente/chat
|
||||||
|
func PostAsistenteChat(c *fiber.Ctx) error {
|
||||||
|
var req asistenteChatRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if req.Mensaje == "" {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "mensaje requerido"})
|
||||||
|
}
|
||||||
|
|
||||||
|
ai, err := models.GetAgenteBotAiConfig()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(503).JSON(fiber.Map{"error": fmt.Sprintf("Asistente no configurado: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
respuesta, err := services.ProcessAgentMessage(asistenteChatID(c), req.Mensaje, ai)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "respuesta": respuesta})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAsistenteHistorial borra el historial de la conversación del usuario actual.
|
||||||
|
// DELETE /app/api/asistente/historial
|
||||||
|
func DeleteAsistenteHistorial(c *fiber.Ctx) error {
|
||||||
|
if err := models.ClearAgentHistory(asistenteChatID(c)); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ─── Render de vistas ────────────────────────────────────────────────────────
|
// ─── Render de vistas ────────────────────────────────────────────────────────
|
||||||
@@ -408,12 +409,12 @@ func GetCuentasCobro(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func CreateCuentaCobro(c *fiber.Ctx) error {
|
func CreateCuentaCobro(c *fiber.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
ClienteID uint `json:"cliente_id"`
|
ClienteID uint `json:"cliente_id"`
|
||||||
Fecha string `json:"fecha"`
|
Fecha string `json:"fecha"`
|
||||||
Descripcion string `json:"descripcion"`
|
Descripcion string `json:"descripcion"`
|
||||||
Valor float64 `json:"valor"`
|
Valor float64 `json:"valor"`
|
||||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||||
Notas string `json:"notas"`
|
Notas string `json:"notas"`
|
||||||
}
|
}
|
||||||
var req Req
|
var req Req
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
@@ -452,13 +453,13 @@ func UpdateCuentaCobro(c *fiber.Ctx) error {
|
|||||||
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
}
|
}
|
||||||
type Req struct {
|
type Req struct {
|
||||||
Estado string `json:"estado"`
|
Estado string `json:"estado"`
|
||||||
FechaPago string `json:"fecha_pago"`
|
FechaPago string `json:"fecha_pago"`
|
||||||
TransaccionID *uint `json:"transaccion_id"`
|
TransaccionID *uint `json:"transaccion_id"`
|
||||||
Notas string `json:"notas"`
|
Notas string `json:"notas"`
|
||||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||||
Descripcion string `json:"descripcion"`
|
Descripcion string `json:"descripcion"`
|
||||||
Valor float64 `json:"valor"`
|
Valor float64 `json:"valor"`
|
||||||
}
|
}
|
||||||
var req Req
|
var req Req
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
@@ -564,10 +565,10 @@ func UpdateCuentaPagar(c *fiber.Ctx) error {
|
|||||||
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
}
|
}
|
||||||
type Req struct {
|
type Req struct {
|
||||||
Estado string `json:"estado"`
|
Estado string `json:"estado"`
|
||||||
FechaPago string `json:"fecha_pago"`
|
FechaPago string `json:"fecha_pago"`
|
||||||
TransaccionID *uint `json:"transaccion_id"`
|
TransaccionID *uint `json:"transaccion_id"`
|
||||||
Notas string `json:"notas"`
|
Notas string `json:"notas"`
|
||||||
}
|
}
|
||||||
var req Req
|
var req Req
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
@@ -614,6 +615,21 @@ func MarcarCuentaPagarPagada(c *fiber.Ctx) error {
|
|||||||
return c.JSON(fiber.Map{"ok": true})
|
return c.JSON(fiber.Map{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerarDocumentoCuentaCobro produce el PDF de solicitud de cuenta de cobro a
|
||||||
|
// partir de la plantilla activa tipo 'cuenta_cobro'.
|
||||||
|
// POST /api/v2/contabilidad/cuentas-cobro/:id/generar-documento
|
||||||
|
func GenerarDocumentoCuentaCobro(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
doc, err := services.GenerarDocumentoCuentaCobro(uint(id), generadoPorFromContext(c))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteCuentaPagar(c *fiber.Ctx) error {
|
func DeleteCuentaPagar(c *fiber.Ctx) error {
|
||||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -462,3 +462,19 @@ func MarcarPagoManual(c *fiber.Ctx) error {
|
|||||||
go services.EnviarCorreoConfirmacionPago(uint(id), "manual")
|
go services.EnviarCorreoConfirmacionPago(uint(id), "manual")
|
||||||
return c.JSON(fiber.Map{"ok": true, "mensaje": "Pago marcado como confirmado"})
|
return c.JSON(fiber.Map{"ok": true, "mensaje": "Pago marcado como confirmado"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerarDocumentoContrato produce el PDF del contrato (cláusulas estándar) a partir
|
||||||
|
// de la plantilla activa tipo 'contrato'. La lógica real vive en
|
||||||
|
// services.GenerarDocumentoContrato, compartida con la tool del agente.
|
||||||
|
// POST /api/v2/contratos/:id/generar-documento
|
||||||
|
func GenerarDocumentoContrato(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
doc, err := services.GenerarDocumentoContrato(uint(id), generadoPorFromContext(c))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type crearCotizacionRequest struct {
|
||||||
|
ClienteID uint `json:"cliente_id"`
|
||||||
|
Alcance string `json:"alcance"`
|
||||||
|
TipoProyecto string `json:"tipo_proyecto"`
|
||||||
|
Items []services.ItemCotizacion `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCotizacion genera el PDF de una cotización a partir de la plantilla activa
|
||||||
|
// tipo 'cotizacion' + los items ya calculados por quien llama (Claude decide los
|
||||||
|
// valores consultando /api/v2/tarifas, el backend solo arma y suma el documento).
|
||||||
|
// La lógica real vive en services.CrearCotizacion, compartida con la tool del agente.
|
||||||
|
// POST /api/v2/cotizaciones
|
||||||
|
func CreateCotizacion(c *fiber.Ctx) error {
|
||||||
|
var req crearCotizacionRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, total, err := services.CrearCotizacion(req.ClienteID, req.Alcance, req.TipoProyecto, req.Items, generadoPorFromContext(c))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(201).JSON(fiber.Map{
|
||||||
|
"ok": true,
|
||||||
|
"message": fmt.Sprintf("Cotización #%d generada", doc.ID),
|
||||||
|
"total": total,
|
||||||
|
"documento": doc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// generadoPorFromContext distingue si la petición vino del dashboard (sesión web,
|
||||||
|
// AuthApi) o de la API admin/agente (AdminApiAuth con ADMIN_API_KEY / bot).
|
||||||
|
func generadoPorFromContext(c *fiber.Ctx) string {
|
||||||
|
if c.Get("X-Agent-Channel") == "telegram" {
|
||||||
|
return "telegram"
|
||||||
|
}
|
||||||
|
if c.Get("Authorization") != "" || c.Get("X-API-Key") != "" {
|
||||||
|
return "claude_api"
|
||||||
|
}
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDocumentosGenerados lista el historial de documentos producidos por la
|
||||||
|
// automatización con IA (cotizaciones, contratos, actas, cuentas de cobro),
|
||||||
|
// sin importar el canal que los generó.
|
||||||
|
func GetDocumentosGenerados(c *fiber.Ctx) error {
|
||||||
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
clienteID, _ := strconv.ParseUint(c.Query("cliente_id", "0"), 10, 32)
|
||||||
|
records, total, err := models.GetAllDocumentosGenerados(limit, offset, c.Query("tipo"), uint(clienteID))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"registros": records,
|
||||||
|
"total": total,
|
||||||
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadDocumentoGenerado sirve el PDF ya generado.
|
||||||
|
func DownloadDocumentoGenerado(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
doc, err := models.GetDocumentoGeneradoByID(uint(id))
|
||||||
|
if err != nil || doc.Archivo == "" {
|
||||||
|
return c.Status(404).JSON(fiber.Map{"error": "Documento no encontrado"})
|
||||||
|
}
|
||||||
|
clean := filepath.Clean(doc.Archivo)
|
||||||
|
if !strings.HasPrefix(clean, "uploads/") {
|
||||||
|
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
||||||
|
}
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, doc.Nombre))
|
||||||
|
return c.SendFile(clean)
|
||||||
|
}
|
||||||
@@ -143,6 +143,7 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
|||||||
{"method": "GET", "path": "/api/v2/contratos/:id/historial", "desc": "Historial del contrato"},
|
{"method": "GET", "path": "/api/v2/contratos/:id/historial", "desc": "Historial del contrato"},
|
||||||
{"method": "POST", "path": "/api/v2/contratos/:id/verificar-pago", "desc": "Verificar pago Bold"},
|
{"method": "POST", "path": "/api/v2/contratos/:id/verificar-pago", "desc": "Verificar pago Bold"},
|
||||||
{"method": "POST", "path": "/api/v2/contratos/:id/marcar-pagado", "desc": "Marcar pago manual"},
|
{"method": "POST", "path": "/api/v2/contratos/:id/marcar-pagado", "desc": "Marcar pago manual"},
|
||||||
|
{"method": "POST", "path": "/api/v2/contratos/:id/generar-documento", "desc": "Genera el PDF del contrato (cláusulas estándar) desde la plantilla activa tipo 'contrato'"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290,6 +291,7 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
|||||||
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-cobro", "desc": "Crear cuenta por cobrar", "body": "cliente_id, descripcion, valor, fecha, fecha_vencimiento, notas"},
|
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-cobro", "desc": "Crear cuenta por cobrar", "body": "cliente_id, descripcion, valor, fecha, fecha_vencimiento, notas"},
|
||||||
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Actualizar cuenta por cobrar"},
|
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Actualizar cuenta por cobrar"},
|
||||||
{"method": "DELETE", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Eliminar cuenta por cobrar"},
|
{"method": "DELETE", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Eliminar cuenta por cobrar"},
|
||||||
|
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-cobro/:id/generar-documento", "desc": "Genera el PDF de solicitud de cuenta de cobro desde la plantilla activa tipo 'cuenta_cobro'"},
|
||||||
{"method": "GET", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Listar cuentas por pagar. Query: ?page=&search=&estado="},
|
{"method": "GET", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Listar cuentas por pagar. Query: ?page=&search=&estado="},
|
||||||
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Crear cuenta por pagar", "body": "entidad_id, descripcion, valor, fecha, vencimiento, notas"},
|
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Crear cuenta por pagar", "body": "entidad_id, descripcion, valor, fecha, vencimiento, notas"},
|
||||||
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-pagar/:id", "desc": "Actualizar cuenta por pagar"},
|
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-pagar/:id", "desc": "Actualizar cuenta por pagar"},
|
||||||
@@ -494,6 +496,45 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
|||||||
{"method": "GET", "path": "/api/v2/vcard-api/miniwebs", "desc": "Listar miniwebs"},
|
{"method": "GET", "path": "/api/v2/vcard-api/miniwebs", "desc": "Listar miniwebs"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"nombre": "Automatización IA: Plantillas de documento",
|
||||||
|
"endpoints": []fiber.Map{
|
||||||
|
{"method": "GET", "path": "/api/v2/plantillas-documento", "desc": "Listar plantillas. Query: ?tipo=cotizacion|contrato|acta|cuenta_cobro&search="},
|
||||||
|
{"method": "GET", "path": "/api/v2/plantillas-documento/:id", "desc": "Detalle de una plantilla"},
|
||||||
|
{"method": "POST", "path": "/api/v2/plantillas-documento", "desc": "Crear plantilla", "body": "tipo, nombre, contenido_html, version, activa"},
|
||||||
|
{"method": "PUT", "path": "/api/v2/plantillas-documento/:id", "desc": "Actualizar plantilla"},
|
||||||
|
{"method": "DELETE", "path": "/api/v2/plantillas-documento/:id", "desc": "Eliminar plantilla"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nombre": "Automatización IA: Tarifas",
|
||||||
|
"endpoints": []fiber.Map{
|
||||||
|
{"method": "GET", "path": "/api/v2/tarifas", "desc": "Listar tarifas. Query: ?categoria=hora_servicio|licencia|vm_azure|margen|otro&search="},
|
||||||
|
{"method": "POST", "path": "/api/v2/tarifas", "desc": "Crear tarifa", "body": "categoria, nombre, valor, moneda, unidad, notas"},
|
||||||
|
{"method": "PUT", "path": "/api/v2/tarifas/:id", "desc": "Actualizar tarifa"},
|
||||||
|
{"method": "DELETE", "path": "/api/v2/tarifas/:id", "desc": "Eliminar tarifa"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nombre": "Automatización IA: Arquitecturas",
|
||||||
|
"endpoints": []fiber.Map{
|
||||||
|
{"method": "GET", "path": "/api/v2/arquitecturas", "desc": "Listar patrones de arquitectura de referencia. Query: ?search=&solo_referencias=true"},
|
||||||
|
{"method": "POST", "path": "/api/v2/arquitecturas", "desc": "Genera el PDF de una propuesta técnica ya redactada", "body": "requerimiento, propuesta, nombre, cliente_id, guardar_como_referencia"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nombre": "Automatización IA: Cotizaciones",
|
||||||
|
"endpoints": []fiber.Map{
|
||||||
|
{"method": "POST", "path": "/api/v2/cotizaciones", "desc": "Genera el PDF de una cotización desde la plantilla activa. Consultar /api/v2/tarifas antes para calcular los items", "body": "cliente_id, alcance, tipo_proyecto, items: [{descripcion, cantidad, valor_unitario, unidad}]"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nombre": "Automatización IA: Historial de documentos generados",
|
||||||
|
"endpoints": []fiber.Map{
|
||||||
|
{"method": "GET", "path": "/api/v2/documentos-generados", "desc": "Historial de documentos generados. Query: ?tipo=&cliente_id="},
|
||||||
|
{"method": "GET", "path": "/api/v2/documentos-generados/:id/download", "desc": "Descargar el PDF generado"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return c.JSON(spec)
|
return c.JSON(spec)
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─── Plantillas de documento (cotización, contrato, acta, cuenta de cobro) ────
|
||||||
|
|
||||||
|
func PlantillasDocumentoView(c *fiber.Ctx) error {
|
||||||
|
return c.Render("automatizacion/plantillas_documento", fiber.Map{
|
||||||
|
"user": c.Locals("user"),
|
||||||
|
"modules": c.Locals("userModules"),
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TarifasView(c *fiber.Ctx) error {
|
||||||
|
return c.Render("automatizacion/tarifas", fiber.Map{
|
||||||
|
"user": c.Locals("user"),
|
||||||
|
"modules": c.Locals("userModules"),
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlantillasDocumento(c *fiber.Ctx) error {
|
||||||
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
records, total, err := models.GetAllPlantillasDocumento(limit, offset, c.Query("search"), c.Query("tipo"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"registros": records,
|
||||||
|
"total": total,
|
||||||
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlantillaDocumento(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
item, err := models.GetPlantillaDocumentoByID(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(404).JSON(fiber.Map{"error": "No encontrada"})
|
||||||
|
}
|
||||||
|
return c.JSON(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePlantillaDocumento(c *fiber.Ctx) error {
|
||||||
|
var m models.PlantillaDocumento
|
||||||
|
if err := c.BodyParser(&m); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if m.ContenidoHTML != "" {
|
||||||
|
if _, err := template.New("validate").Parse(m.ContenidoHTML); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "Sintaxis inválida en el HTML: " + err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := models.CreatePlantillaDocumento(m); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(fiber.Map{"message": "Plantilla creada", "ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdatePlantillaDocumento(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
var updates map[string]interface{}
|
||||||
|
if err := c.BodyParser(&updates); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if html, ok := updates["contenido_html"].(string); ok && html != "" {
|
||||||
|
if _, err := template.New("validate").Parse(html); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "Sintaxis inválida en el HTML: " + err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := models.UpdatePlantillaDocumento(uint(id), updates); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePlantillaDocumento(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
if err := models.DeletePlantillaDocumento(uint(id)); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tarifas ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func GetTarifas(c *fiber.Ctx) error {
|
||||||
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "50"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
records, total, err := models.GetAllTarifas(limit, offset, c.Query("search"), c.Query("categoria"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"registros": records,
|
||||||
|
"total": total,
|
||||||
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTarifa(c *fiber.Ctx) error {
|
||||||
|
var m models.Tarifa
|
||||||
|
if err := c.BodyParser(&m); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if err := models.CreateTarifa(m); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(201).JSON(fiber.Map{"message": "Tarifa creada", "ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateTarifa(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
var updates map[string]interface{}
|
||||||
|
if err := c.BodyParser(&updates); err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
if err := models.UpdateTarifa(uint(id), updates); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTarifa(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||||
|
}
|
||||||
|
if err := models.DeleteTarifa(uint(id)); err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
|
||||||
|
}
|
||||||
@@ -104,6 +104,7 @@ func AdminApiRoutes(api fiber.Router) {
|
|||||||
h.Get("/contratos/:id/historial", controllers.GetHistorialContrato)
|
h.Get("/contratos/:id/historial", controllers.GetHistorialContrato)
|
||||||
h.Post("/contratos/:id/verificar-pago", controllers.VerificarPagoBold)
|
h.Post("/contratos/:id/verificar-pago", controllers.VerificarPagoBold)
|
||||||
h.Post("/contratos/:id/marcar-pagado", controllers.MarcarPagoManual)
|
h.Post("/contratos/:id/marcar-pagado", controllers.MarcarPagoManual)
|
||||||
|
h.Post("/contratos/:id/generar-documento", controllers.GenerarDocumentoContrato)
|
||||||
|
|
||||||
// ─── Plantillas de correo ────────────────────────────────────────────────
|
// ─── Plantillas de correo ────────────────────────────────────────────────
|
||||||
h.Get("/plantillas-correo", controllers.GetPlantillas)
|
h.Get("/plantillas-correo", controllers.GetPlantillas)
|
||||||
@@ -218,6 +219,7 @@ func AdminApiRoutes(api fiber.Router) {
|
|||||||
h.Post("/contabilidad/cuentas-cobro", controllers.CreateCuentaCobro)
|
h.Post("/contabilidad/cuentas-cobro", controllers.CreateCuentaCobro)
|
||||||
h.Put("/contabilidad/cuentas-cobro/:id", controllers.UpdateCuentaCobro)
|
h.Put("/contabilidad/cuentas-cobro/:id", controllers.UpdateCuentaCobro)
|
||||||
h.Delete("/contabilidad/cuentas-cobro/:id", controllers.DeleteCuentaCobro)
|
h.Delete("/contabilidad/cuentas-cobro/:id", controllers.DeleteCuentaCobro)
|
||||||
|
h.Post("/contabilidad/cuentas-cobro/:id/generar-documento", controllers.GenerarDocumentoCuentaCobro)
|
||||||
// Cuentas por pagar
|
// Cuentas por pagar
|
||||||
h.Get("/contabilidad/cuentas-pagar", controllers.GetCuentasPagar)
|
h.Get("/contabilidad/cuentas-pagar", controllers.GetCuentasPagar)
|
||||||
h.Post("/contabilidad/cuentas-pagar", controllers.CreateCuentaPagar)
|
h.Post("/contabilidad/cuentas-pagar", controllers.CreateCuentaPagar)
|
||||||
@@ -401,6 +403,30 @@ func AdminApiRoutes(api fiber.Router) {
|
|||||||
h.Delete("/agent/auth/:id", controllers.AgentAuthDelete)
|
h.Delete("/agent/auth/:id", controllers.AgentAuthDelete)
|
||||||
h.Delete("/agent/history/:chat_id", controllers.AgentHistoryClear)
|
h.Delete("/agent/history/:chat_id", controllers.AgentHistoryClear)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Cotizaciones ─────────────────────────────────────
|
||||||
|
h.Post("/cotizaciones", controllers.CreateCotizacion)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Arquitecturas ────────────────────────────────────
|
||||||
|
h.Get("/arquitecturas", controllers.GetArquitecturas)
|
||||||
|
h.Post("/arquitecturas", controllers.CreateArquitectura)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Historial de documentos generados ───────────────
|
||||||
|
h.Get("/documentos-generados", controllers.GetDocumentosGenerados)
|
||||||
|
h.Get("/documentos-generados/:id/download", controllers.DownloadDocumentoGenerado)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Plantillas de documento ─────────────────────────
|
||||||
|
h.Get("/plantillas-documento", controllers.GetPlantillasDocumento)
|
||||||
|
h.Get("/plantillas-documento/:id", controllers.GetPlantillaDocumento)
|
||||||
|
h.Post("/plantillas-documento", controllers.CreatePlantillaDocumento)
|
||||||
|
h.Put("/plantillas-documento/:id", controllers.UpdatePlantillaDocumento)
|
||||||
|
h.Delete("/plantillas-documento/:id", controllers.DeletePlantillaDocumento)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Tarifas ──────────────────────────────────────────
|
||||||
|
h.Get("/tarifas", controllers.GetTarifas)
|
||||||
|
h.Post("/tarifas", controllers.CreateTarifa)
|
||||||
|
h.Put("/tarifas/:id", controllers.UpdateTarifa)
|
||||||
|
h.Delete("/tarifas/:id", controllers.DeleteTarifa)
|
||||||
|
|
||||||
// ─── VCard API ───────────────────────────────────────────────────────────
|
// ─── VCard API ───────────────────────────────────────────────────────────
|
||||||
h.Get("/vcard-api/config", controllers.VcardApiGetConfig)
|
h.Get("/vcard-api/config", controllers.VcardApiGetConfig)
|
||||||
h.Get("/vcard-api/usuarios", controllers.VcardApiUsuarios)
|
h.Get("/vcard-api/usuarios", controllers.VcardApiUsuarios)
|
||||||
|
|||||||
@@ -71,6 +71,36 @@ func RenovacionesRoutes(protected fiber.Router) {
|
|||||||
protected.Post("/api/smtp-config", controllers.SaveSmtpConfig)
|
protected.Post("/api/smtp-config", controllers.SaveSmtpConfig)
|
||||||
protected.Post("/api/smtp-config/test", controllers.TestSmtpConfig)
|
protected.Post("/api/smtp-config/test", controllers.TestSmtpConfig)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Asistente (chat propio) ───────────────────
|
||||||
|
protected.Get("/asistente", middlewares.MenuMiddleware, controllers.AsistenteView)
|
||||||
|
protected.Post("/api/asistente/chat", controllers.PostAsistenteChat)
|
||||||
|
protected.Delete("/api/asistente/historial", controllers.DeleteAsistenteHistorial)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Plantillas de documento ────────────────────
|
||||||
|
protected.Get("/automatizacion/plantillas", middlewares.MenuMiddleware, controllers.PlantillasDocumentoView)
|
||||||
|
protected.Get("/api/plantillas-documento", controllers.GetPlantillasDocumento)
|
||||||
|
protected.Get("/api/plantillas-documento/:id", controllers.GetPlantillaDocumento)
|
||||||
|
protected.Post("/api/plantillas-documento", controllers.CreatePlantillaDocumento)
|
||||||
|
protected.Put("/api/plantillas-documento/:id", controllers.UpdatePlantillaDocumento)
|
||||||
|
protected.Delete("/api/plantillas-documento/:id", controllers.DeletePlantillaDocumento)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Tarifas ─────────────────────────────────────
|
||||||
|
protected.Get("/automatizacion/tarifas", middlewares.MenuMiddleware, controllers.TarifasView)
|
||||||
|
protected.Get("/api/tarifas", controllers.GetTarifas)
|
||||||
|
protected.Post("/api/tarifas", controllers.CreateTarifa)
|
||||||
|
protected.Put("/api/tarifas/:id", controllers.UpdateTarifa)
|
||||||
|
protected.Delete("/api/tarifas/:id", controllers.DeleteTarifa)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Arquitecturas ───────────────────────────────
|
||||||
|
protected.Get("/api/arquitecturas", controllers.GetArquitecturas)
|
||||||
|
protected.Post("/api/arquitecturas", controllers.CreateArquitectura)
|
||||||
|
|
||||||
|
// ─── Automatización IA: Cotizaciones y documentos generados ────────
|
||||||
|
protected.Post("/api/cotizaciones", controllers.CreateCotizacion)
|
||||||
|
protected.Post("/api/contratos/:id/generar-documento", controllers.GenerarDocumentoContrato)
|
||||||
|
protected.Get("/api/documentos-generados", controllers.GetDocumentosGenerados)
|
||||||
|
protected.Get("/api/documentos-generados/:id/download", controllers.DownloadDocumentoGenerado)
|
||||||
|
|
||||||
// ─── Diagnóstico ──────────────────────────────────────────────────
|
// ─── Diagnóstico ──────────────────────────────────────────────────
|
||||||
protected.Post("/api/diagnostico/ciclo-notificaciones", controllers.EjecutarTestCiclo)
|
protected.Post("/api/diagnostico/ciclo-notificaciones", controllers.EjecutarTestCiclo)
|
||||||
|
|
||||||
@@ -79,4 +109,8 @@ func RenovacionesRoutes(protected fiber.Router) {
|
|||||||
migrations.SeedRenovaciones()
|
migrations.SeedRenovaciones()
|
||||||
return c.JSON(fiber.Map{"ok": true, "message": "Seed de Renovaciones ejecutado correctamente"})
|
return c.JSON(fiber.Map{"ok": true, "message": "Seed de Renovaciones ejecutado correctamente"})
|
||||||
})
|
})
|
||||||
|
protected.Post("/api/admin/seed-automatizacion-ia", func(c *fiber.Ctx) error {
|
||||||
|
migrations.SeedAutomatizacionIA()
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "message": "Seed de Automatización IA ejecutado correctamente"})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -47,13 +47,13 @@ func UserRoutes(app fiber.Router) {
|
|||||||
protected.Delete("/submodules/:id", controllers.DeleteSubmodule) // Eliminar un módulo
|
protected.Delete("/submodules/:id", controllers.DeleteSubmodule) // Eliminar un módulo
|
||||||
|
|
||||||
// Rutas de usuarios
|
// Rutas de usuarios
|
||||||
protected.Get("/users", middlewares.MenuMiddleware, controllers.Users) // Renderizar la vista
|
protected.Get("/users", middlewares.MenuMiddleware, controllers.Users) // Renderizar la vista
|
||||||
protected.Get("/loadusers", controllers.GetUsers) // Obtener
|
protected.Get("/loadusers", controllers.GetUsers) // Obtener
|
||||||
protected.Post("/users", controllers.CreateUser) // Crear
|
protected.Post("/users", controllers.CreateUser) // Crear
|
||||||
protected.Put("/users/:id", controllers.UpdateUser) // Actualizar
|
protected.Put("/users/:id", controllers.UpdateUser) // Actualizar
|
||||||
protected.Put("/password/:id", controllers.UpdatePassword) // Actualizar
|
protected.Put("/password/:id", controllers.UpdatePassword) // Actualizar
|
||||||
protected.Delete("/users/:id", controllers.DeleteUser) // Eliminar
|
protected.Delete("/users/:id", controllers.DeleteUser) // Eliminar
|
||||||
protected.Get("/user/:id", controllers.GetUser) // Buscar un usuario
|
protected.Get("/user/:id", controllers.GetUser) // Buscar un usuario
|
||||||
protected.Post("/users/:id/credenciales", controllers.EnviarCredencialesUsuario) // Enviar credenciales
|
protected.Post("/users/:id/credenciales", controllers.EnviarCredencialesUsuario) // Enviar credenciales
|
||||||
|
|
||||||
// Rutas de conexiones ssh
|
// Rutas de conexiones ssh
|
||||||
@@ -486,9 +486,9 @@ func UserRoutes(app fiber.Router) {
|
|||||||
|
|
||||||
// ─── Contabilidad ─────────────────────────────────────────────────────────
|
// ─── Contabilidad ─────────────────────────────────────────────────────────
|
||||||
protected.Get("/contabilidad", middlewares.MenuMiddleware, controllers.ContabilidadIndex)
|
protected.Get("/contabilidad", middlewares.MenuMiddleware, controllers.ContabilidadIndex)
|
||||||
protected.Get("/contabilidad/dashboard", controllers.ContabilidadDashboard)
|
protected.Get("/contabilidad/dashboard", controllers.ContabilidadDashboard)
|
||||||
protected.Get("/contabilidad/consolidado", controllers.ContabilidadConsolidado)
|
protected.Get("/contabilidad/consolidado", controllers.ContabilidadConsolidado)
|
||||||
protected.Get("/contabilidad/consolidados", controllers.ContabilidadListConsolidados)
|
protected.Get("/contabilidad/consolidados", controllers.ContabilidadListConsolidados)
|
||||||
|
|
||||||
// Transacciones
|
// Transacciones
|
||||||
protected.Get("/contabilidad/transacciones", middlewares.MenuMiddleware, controllers.ContabilidadTransaccionesView)
|
protected.Get("/contabilidad/transacciones", middlewares.MenuMiddleware, controllers.ContabilidadTransaccionesView)
|
||||||
@@ -519,6 +519,7 @@ protected.Get("/contabilidad/consolidados", controllers.ContabilidadListConsolid
|
|||||||
protected.Post("/contabilidad/cuentas-cobro", controllers.CreateCuentaCobro)
|
protected.Post("/contabilidad/cuentas-cobro", controllers.CreateCuentaCobro)
|
||||||
protected.Put("/contabilidad/cuentas-cobro/:id", controllers.UpdateCuentaCobro)
|
protected.Put("/contabilidad/cuentas-cobro/:id", controllers.UpdateCuentaCobro)
|
||||||
protected.Delete("/contabilidad/cuentas-cobro/:id", controllers.DeleteCuentaCobro)
|
protected.Delete("/contabilidad/cuentas-cobro/:id", controllers.DeleteCuentaCobro)
|
||||||
|
protected.Post("/contabilidad/cuentas-cobro/:id/generar-documento", controllers.GenerarDocumentoCuentaCobro)
|
||||||
|
|
||||||
// Cuentas por pagar
|
// Cuentas por pagar
|
||||||
protected.Get("/contabilidad/cuentas-pagar", middlewares.MenuMiddleware, controllers.ContabilidadPagarView)
|
protected.Get("/contabilidad/cuentas-pagar", middlewares.MenuMiddleware, controllers.ContabilidadPagarView)
|
||||||
|
|||||||
Reference in New Issue
Block a user