mirror of
https://github.com/hazemKrimi/crimson-vault.git
synced 2026-05-01 18:20:27 +00:00
wip: improve invoice generation logic
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ to quickly create a Cobra application.`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
dir, err := lib.GetConfigDirectory()
|
||||
dir, err := lib.GetConfigDirectoryPath()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/johnfercher/maroto/v2 v2.3.1
|
||||
github.com/labstack/echo-contrib v0.17.4
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/spf13/cobra v1.9.1
|
||||
@@ -38,7 +39,6 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/johnfercher/go-tree v1.0.5 // indirect
|
||||
github.com/johnfercher/maroto/v2 v2.3.1 // indirect
|
||||
github.com/jung-kurt/gofpdf v1.16.2 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
||||
@@ -104,6 +104,10 @@ github.com/johnfercher/maroto/v2 v2.3.1/go.mod h1:/LfW6AQGZzsG6xUixcfyxkKztDoszd
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
|
||||
@@ -142,6 +146,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
@@ -149,6 +155,8 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -258,6 +266,8 @@ golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
+83
-2
@@ -85,9 +85,26 @@ func (api *API) CreateInvoiceHandler(context echo.Context) error {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Unexpected error getting User."), Messages: []string{"Unexpected error getting User!"}}
|
||||
}
|
||||
|
||||
lib.GenerateInvoice(invoice, user, client)
|
||||
path, err := lib.GenerateInvoice(invoice, user, client)
|
||||
|
||||
return context.Attachment(fmt.Sprintf("invoices/%s.pdf", invoice.ID), fmt.Sprintf("invoices/%s.pdf", invoice.ID))
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error generating Invoice!"}}
|
||||
}
|
||||
|
||||
invoiceFileName := fmt.Sprintf("%s.pdf", invoice.ID)
|
||||
invoiceId, err := uuid.Parse(invoice.ID)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusBadRequest, Messages: []string{"Could not parse Invoice ID!"}}
|
||||
}
|
||||
|
||||
err = api.db.UpdateInvoicePDF(userId, invoiceId, path, &invoice)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error generating Invoice!"}}
|
||||
}
|
||||
|
||||
return context.Attachment(path, invoiceFileName)
|
||||
}
|
||||
|
||||
func (api *API) GetAllItemsHandler(context echo.Context) error {
|
||||
@@ -172,6 +189,70 @@ func (api *API) GetInvoiceHandler(context echo.Context) error {
|
||||
return context.JSON(http.StatusOK, invoice)
|
||||
}
|
||||
|
||||
func (api *API) DownloadInvoiceHandler(context echo.Context) error {
|
||||
userId, err := uuid.Parse(context.Get("id").(string))
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Session ID not found after authorization."), Messages: []string{"Unexpected error getting User!"}}
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(context.Param("id"))
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusBadRequest, Messages: []string{"ID is required to download an Invoice!"}}
|
||||
}
|
||||
|
||||
var invoice types.Invoice
|
||||
|
||||
if err := api.db.GetInvoiceById(userId, id, &invoice); err != nil {
|
||||
return types.Error{Code: http.StatusNotFound, Messages: []string{"Invoice not found!"}}
|
||||
}
|
||||
|
||||
if invoice.PDF != "" {
|
||||
invoiceFileName := fmt.Sprintf("%s.pdf", invoice.ID)
|
||||
return context.Attachment(invoice.PDF, invoiceFileName)
|
||||
}
|
||||
|
||||
var user types.User
|
||||
|
||||
if err := api.db.GetUserById(userId, &user); err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Unexpected error getting User."), Messages: []string{"Unexpected error getting User!"}}
|
||||
}
|
||||
|
||||
clientId, err := uuid.Parse(invoice.ClientID)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Invalid Client ID."), Messages: []string{"Unexpected error getting User!"}}
|
||||
}
|
||||
|
||||
var client types.Client
|
||||
|
||||
if err := api.db.GetClientById(userId, clientId, &client); err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: errors.New("Unexpected error getting Client."), Messages: []string{"Unexpected error getting User!"}}
|
||||
}
|
||||
|
||||
path, err := lib.GenerateInvoice(invoice, user, client)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error generating Invoice!"}}
|
||||
}
|
||||
|
||||
invoiceFileName := fmt.Sprintf("%s.pdf", invoice.ID)
|
||||
invoiceId, err := uuid.Parse(invoice.ID)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusBadRequest, Messages: []string{"Could not parse Invoice ID!"}}
|
||||
}
|
||||
|
||||
err = api.db.UpdateInvoicePDF(userId, invoiceId, path, &invoice)
|
||||
|
||||
if err != nil {
|
||||
return types.Error{Code: http.StatusInternalServerError, Cause: err, Messages: []string{"Unexpected error generating Invoice!"}}
|
||||
}
|
||||
|
||||
return context.Attachment(path, invoiceFileName)
|
||||
}
|
||||
|
||||
func (api *API) UpdateItemHandler(context echo.Context) error {
|
||||
userId, err := uuid.Parse(context.Get("id").(string))
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ func (api *API) InvoiceRoutes() {
|
||||
invoices.POST("/", api.CreateInvoiceHandler)
|
||||
invoices.POST("/:id/items/", api.CreateItemHandler)
|
||||
invoices.GET("/:id/", api.GetInvoiceHandler)
|
||||
invoices.GET("/:id/download/", api.DownloadInvoiceHandler)
|
||||
invoices.GET("/:id/items/", api.GetAllItemsHandler)
|
||||
invoices.GET("/items/:id/", api.GetItemHandler)
|
||||
invoices.PUT("/:id/", api.UpdateInvoiceHandler)
|
||||
|
||||
+22
-34
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/johnfercher/maroto/v2"
|
||||
"github.com/johnfercher/maroto/v2/pkg/components/code"
|
||||
"github.com/johnfercher/maroto/v2/pkg/components/col"
|
||||
"github.com/johnfercher/maroto/v2/pkg/components/image"
|
||||
"github.com/johnfercher/maroto/v2/pkg/components/row"
|
||||
@@ -25,7 +24,7 @@ import (
|
||||
"github.com/hazemKrimi/crimson-vault/internal/types"
|
||||
)
|
||||
|
||||
func GetConfigDirectory() (string, error) {
|
||||
func GetConfigDirectoryPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
|
||||
if err != nil {
|
||||
@@ -172,7 +171,7 @@ func getTransactions(items []types.Item) []core.Row {
|
||||
col.New(3),
|
||||
text.NewCol(4, item.Name, props.Text{Size: 8, Align: align.Center}),
|
||||
text.NewCol(2, fmt.Sprintf("%d", item.Quantity), props.Text{Size: 8, Align: align.Center}),
|
||||
text.NewCol(3, fmt.Sprintf("%d", item.Price), props.Text{Size: 8, Align: align.Center}),
|
||||
text.NewCol(3, fmt.Sprintf("%.2f", item.Price), props.Text{Size: 8, Align: align.Center}),
|
||||
)
|
||||
if i%2 == 0 {
|
||||
gray := getGrayColor()
|
||||
@@ -203,7 +202,7 @@ func getTransactions(items []types.Item) []core.Row {
|
||||
return rows
|
||||
}
|
||||
|
||||
func GenerateInvoice(invoice types.Invoice, user types.User, client types.Client) error {
|
||||
func GenerateInvoice(invoice types.Invoice, user types.User, client types.Client) (string, error) {
|
||||
cfg := config.NewBuilder().WithPageNumber().WithLeftMargin(10).WithRightMargin(10).WithTopMargin(15).Build()
|
||||
darkGray := getDarkGrayColor()
|
||||
mrt := maroto.New(cfg)
|
||||
@@ -212,23 +211,23 @@ func GenerateInvoice(invoice types.Invoice, user types.User, client types.Client
|
||||
err := m.RegisterHeader(getPageHeader(client, user.Logo))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = m.RegisterFooter(getPageFooter(user))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.AddRows(text.NewRow(10, "Invoice ABC123456789", props.Text{
|
||||
m.AddRows(text.NewRow(10, fmt.Sprintf("Invoice %s", invoice.ID), props.Text{
|
||||
Top: 3,
|
||||
Style: fontstyle.Bold,
|
||||
Align: align.Center,
|
||||
}))
|
||||
|
||||
m.AddRow(7,
|
||||
text.NewCol(3, "Transactions", props.Text{
|
||||
text.NewCol(3, "Items", props.Text{
|
||||
Top: 1.5,
|
||||
Size: 9,
|
||||
Style: fontstyle.Bold,
|
||||
@@ -239,43 +238,32 @@ func GenerateInvoice(invoice types.Invoice, user types.User, client types.Client
|
||||
|
||||
m.AddRows(getTransactions(invoice.Items)...)
|
||||
|
||||
m.AddRow(15,
|
||||
col.New(6).Add(
|
||||
code.NewBar("5123.151231.512314.1251251.123215", props.Barcode{
|
||||
Percent: 0,
|
||||
Proportion: props.Proportion{
|
||||
Width: 20,
|
||||
Height: 2,
|
||||
},
|
||||
}),
|
||||
text.New("5123.151231.512314.1251251.123215", props.Text{
|
||||
Top: 12,
|
||||
Family: "",
|
||||
Style: fontstyle.Bold,
|
||||
Size: 9,
|
||||
Align: align.Center,
|
||||
}),
|
||||
),
|
||||
col.New(6),
|
||||
)
|
||||
|
||||
document, err := m.Generate()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = document.Save(fmt.Sprintf("invoices/%s.pdf", invoice.ID))
|
||||
configDir, err := GetConfigDirectoryPath()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = document.GetReport().Save(fmt.Sprintf("invoices/%s.txt", invoice.ID))
|
||||
invoicesDir := filepath.Join(configDir, user.Username, client.ID)
|
||||
|
||||
if err := os.MkdirAll(invoicesDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
invoiceFileName := fmt.Sprintf("%s.pdf", invoice.ID)
|
||||
invoicePath := filepath.Join(invoicesDir, invoiceFileName)
|
||||
|
||||
err = document.Save(invoicePath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return nil
|
||||
return invoicePath, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -170,6 +171,24 @@ func (db *DB) UpdateInvoice(userId, id uuid.UUID, body types.UpdateInvoiceReques
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) UpdateInvoicePDF(userId, id uuid.UUID, pdf string, invoice *types.Invoice) error {
|
||||
if pdf == "" {
|
||||
return errors.New("PDF path is empty!")
|
||||
}
|
||||
|
||||
result := db.instance.Where("user_id = ?", userId).Where("id = ?", id).First(invoice)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
result = db.instance.Model(invoice).Updates(types.Invoice{
|
||||
PDF: pdf,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteItem(userId, id uuid.UUID) error {
|
||||
result := db.instance.Unscoped().Where("user_id = ?", userId).Where("id = ?", id).Delete(&types.Item{})
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type Item struct {
|
||||
UserID string `json:"userId" gorm:"type:varchar(255)"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Price uint32 `json:"price"`
|
||||
Price float32 `json:"price"`
|
||||
Quantity uint32 `json:"quantity"`
|
||||
Tax uint32 `json:"tax"`
|
||||
}
|
||||
@@ -79,7 +79,7 @@ type CreateItemRequestBody struct {
|
||||
Name string `json:"name" validate:"alpha,required"`
|
||||
Type string `json:"type" validate:"alpha,required"`
|
||||
Quantity uint32 `json:"quantity" validate:"number,required"`
|
||||
Price uint32 `json:"price" validate:"number,required"`
|
||||
Price float32 `json:"price" validate:"number,required"`
|
||||
Tax uint32 `json:"tax" validate:"number,omitempty"`
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ type UpdateItemRequestBody struct {
|
||||
Name string `json:"name" validate:"alpha,omitempty"`
|
||||
Type string `json:"type" validate:"alpha,omitempty"`
|
||||
Quantity uint32 `json:"quantity" validate:"number,omitempty"`
|
||||
Price uint32 `json:"price" validate:"number,omitempty"`
|
||||
Price float32 `json:"price" validate:"number,omitempty"`
|
||||
Tax uint32 `json:"tax" validate:"number,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Disable the enforcement of foreign-keys constraints
|
||||
PRAGMA foreign_keys = off;
|
||||
-- Create "new_items" table
|
||||
CREATE TABLE `new_items` (
|
||||
`id` varchar NULL,
|
||||
`invoice_id` varchar NULL,
|
||||
`user_id` varchar NULL,
|
||||
`name` text NULL,
|
||||
`type` text NULL,
|
||||
`price` real NULL,
|
||||
`quantity` integer NULL,
|
||||
`tax` integer NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `fk_invoices_items` FOREIGN KEY (`invoice_id`) REFERENCES `invoices` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
);
|
||||
-- Copy rows from old table "items" to new temporary table "new_items"
|
||||
INSERT INTO `new_items` (`id`, `invoice_id`, `user_id`, `name`, `type`, `price`, `quantity`, `tax`) SELECT `id`, `invoice_id`, `user_id`, `name`, `type`, `price`, `quantity`, `tax` FROM `items`;
|
||||
-- Drop "items" table after copying rows
|
||||
DROP TABLE `items`;
|
||||
-- Rename temporary table "new_items" to "items"
|
||||
ALTER TABLE `new_items` RENAME TO `items`;
|
||||
-- Enable back the enforcement of foreign-keys constraints
|
||||
PRAGMA foreign_keys = on;
|
||||
@@ -1,5 +1,6 @@
|
||||
h1:dDD/MWKqjFIkCAuR4i/9LtH4ibg9wZR03FApuHHYCos=
|
||||
h1:wDAImuaIK9lhzbnQ3++MNijLAxUnoaLdlysqqYjOfoU=
|
||||
20250611102124.sql h1:HkDgtWXUxfAR9gIObTMzP98pVEIakerASGGSufW695k=
|
||||
20250616102420.sql h1:7C1kEskaDwdHmQOu3t48oZcky0WaNiFeIugJcJFkjKM=
|
||||
20250616150439.sql h1:nx8CvH5om7lbeEezo7roNcTV+f0agch0gBjYxKtTDn8=
|
||||
20250616151658.sql h1:O4hbYFj4bwMDML0mR9PtSn7GUbXQVCbh+lGkwTNBwnU=
|
||||
20250814173803.sql h1:xRBhN15wPcJJOfzzPuMoUJsUqEYNCdLmEBiYUJBF1FA=
|
||||
|
||||
Reference in New Issue
Block a user