1 Commits

Author SHA1 Message Date
fdd3644ec5 fix: cart and products 2026-04-16 12:58:49 +02:00
95 changed files with 1194 additions and 10167 deletions

View File

@@ -28,7 +28,7 @@ tmp_dir = "tmp"
rerun = false rerun = false
rerun_delay = 500 rerun_delay = 500
send_interrupt = false send_interrupt = false
stop_on_error = false stop_on_error = true
[color] [color]
app = "" app = ""

3
.env
View File

@@ -64,6 +64,3 @@ IMAGE_PREFIX=https://www.naluconcept.com # remove prefix to serv them from same
CORS_ORGIN=https://www.naluconcept.com CORS_ORGIN=https://www.naluconcept.com
DSN=root:Maal12345678@tcp(localhost:3306)/nalu DSN=root:Maal12345678@tcp(localhost:3306)/nalu
LOG_LEVEL=warn
LOG_COLORIZE=true

View File

@@ -1,116 +0,0 @@
package orderStatusActions
// func init() {
// GlobalRegistry.Register(enums.OrderStatusConfirmed, ActionChain{
// SendOrderConfirmationEmail,
// NotifyInventorySystem,
// })
// GlobalRegistry.Register(enums.OrderStatusProcessing, ActionChain{
// NotifyWarehouse,
// ReserveInventory,
// })
// GlobalRegistry.Register(enums.OrderStatusShipped, ActionChain{
// NotifyWarehouseShipped,
// GenerateTrackingNumber,
// SendShippingNotificationEmail,
// })
// GlobalRegistry.Register(enums.OrderStatusDelivered, ActionChain{
// SendDeliveryConfirmationEmail,
// NotifyFulfillmentComplete,
// })
// GlobalRegistry.Register(enums.OrderStatusCancelled, ActionChain{
// SendCancellationEmail,
// ReleaseInventory,
// ProcessRefund,
// })
// GlobalRegistry.Register(enums.OrderStatusReturned, ActionChain{
// SendReturnConfirmationEmail,
// NotifyReturnsDepartment,
// })
// GlobalRegistry.Register(enums.OrderStatusRefunded, ActionChain{
// NotifyRefundProcessed,
// })
// GlobalRegistry.Register(enums.OrderStatusPending, ActionChain{})
// }
// var SendOrderConfirmationEmail = WithID("send_order_confirmation_email", func(actionCtx ActionContext) ActionResult {
// log.Printf("Sending order confirmation email for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyInventorySystem = WithID("notify_inventory_system", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying inventory system for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyWarehouse = WithID("notify_warehouse", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying warehouse for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var ReserveInventory = WithID("reserve_inventory", func(actionCtx ActionContext) ActionResult {
// log.Printf("Reserving inventory for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyWarehouseShipped = WithID("notify_warehouse_shipped", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying warehouse of shipment for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var GenerateTrackingNumber = WithID("generate_tracking_number", func(actionCtx ActionContext) ActionResult {
// log.Printf("Generating tracking number for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var SendShippingNotificationEmail = WithID("send_shipping_notification_email", func(actionCtx ActionContext) ActionResult {
// log.Printf("Sending shipping notification email for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var SendDeliveryConfirmationEmail = WithID("send_delivery_confirmation_email", func(actionCtx ActionContext) ActionResult {
// log.Printf("Sending delivery confirmation email for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyFulfillmentComplete = WithID("notify_fulfillment_complete", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying fulfillment complete for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var SendCancellationEmail = WithID("send_cancellation_email", func(actionCtx ActionContext) ActionResult {
// log.Printf("Sending cancellation email for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var ReleaseInventory = WithID("release_inventory", func(actionCtx ActionContext) ActionResult {
// log.Printf("Releasing inventory for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var ProcessRefund = WithID("process_refund", func(actionCtx ActionContext) ActionResult {
// log.Printf("Processing refund for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var SendReturnConfirmationEmail = WithID("send_return_confirmation_email", func(actionCtx ActionContext) ActionResult {
// log.Printf("Sending return confirmation email for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyReturnsDepartment = WithID("notify_returns_department", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying returns department for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })
// var NotifyRefundProcessed = WithID("notify_refund_processed", func(actionCtx ActionContext) ActionResult {
// log.Printf("Notifying refund processed for order %d", actionCtx.OrderId)
// return ActionResult{Err: nil}
// })

View File

@@ -1,21 +0,0 @@
package orderStatusActions
import (
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
)
func init() {
var sendNewOrderEmail = WithID("send_new_order_email", func(actionCtx ActionContext) ActionResult {
if actionCtx.EmailService == nil {
return ActionResult{Err: fmt.Errorf("emailService not provided")}
}
return ActionResult{Err: actionCtx.EmailService.SendNewOrderPlacedNotification(*actionCtx.UserId)}
})
GlobalRegistry.Register(enums.OrderStatusPending, ActionChain{
sendNewOrderEmail,
})
}

View File

@@ -1,88 +0,0 @@
package orderStatusActions
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
)
var GlobalRegistry = make(ActionRegistry)
type ActionID string
type ActionContext struct {
Order *model.CustomerOrder
UserId *uint
EmailService *emailService.EmailService
}
type ActionResult struct {
Err error
Metadata map[string]any
}
type OrderAction interface {
ID() ActionID
Execute(actionCtx ActionContext) ActionResult
}
type ActionChain []OrderAction
func (c ActionChain) Execute(actionCtx ActionContext) []ActionResult {
results := make([]ActionResult, 0, len(c))
for _, action := range c {
result := action.Execute(actionCtx)
results = append(results, result)
if result.Err != nil {
logger.Debug("action failed",
"action_id", action.ID(),
"order", actionCtx.Order,
"user_id", actionCtx.UserId,
"error", result.Err,
)
}
}
return results
}
type ActionRegistry map[enums.OrderStatus]ActionChain
func (r ActionRegistry) Register(status enums.OrderStatus, chain ActionChain) {
r[status] = chain
}
func (r ActionRegistry) ExecuteForStatus(status enums.OrderStatus, actionCtx ActionContext) []ActionResult {
chain, exists := r[status]
if !exists {
return nil
}
return chain.Execute(actionCtx)
}
type ActionFunc func(actionCtx ActionContext) ActionResult
func (f ActionFunc) ID() ActionID {
return "anonymous"
}
func (f ActionFunc) Execute(actionCtx ActionContext) ActionResult {
return f(actionCtx)
}
type actionAdapter struct {
id ActionID
fn ActionFunc
}
func (a *actionAdapter) ID() ActionID {
return a.id
}
func (a *actionAdapter) Execute(actionCtx ActionContext) ActionResult {
return a.fn(actionCtx)
}
func WithID(id ActionID, fn ActionFunc) OrderAction {
return &actionAdapter{id: id, fn: fn}
}

View File

@@ -4,11 +4,8 @@ import (
"log" "log"
"os" "os"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/delivery/web" "git.ma-al.com/goc_daniel/b2b/app/delivery/web"
"git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/service/langsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/version" "git.ma-al.com/goc_daniel/b2b/app/utils/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -24,8 +21,6 @@ var (
return return
} }
logger.Init("b2b", nil, config.Get().Log.LogLevel, config.Get().Log.LogColorize)
// Create and setup the server // Create and setup the server
server := web.New() server := web.New()

View File

@@ -28,7 +28,6 @@ type Config struct {
Cors CorsConfig Cors CorsConfig
MeiliSearch MeiliSearchConfig MeiliSearch MeiliSearchConfig
Storage StorageConfig Storage StorageConfig
Log LogConfig
} }
type I18n struct { type I18n struct {
@@ -88,11 +87,6 @@ type AppConfig struct {
BaseURL string `env:"APP_BASE_URL,http://localhost:5173"` BaseURL string `env:"APP_BASE_URL,http://localhost:5173"`
} }
type LogConfig struct {
LogLevel string `env:"LOG_LEVEL,warn"`
LogColorize bool `env:"LOG_COLORIZE,true"`
}
type EmailConfig struct { type EmailConfig struct {
SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"` SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"`
SMTPPort int `env:"EMAIL_SMTP_PORT,587"` SMTPPort int `env:"EMAIL_SMTP_PORT,587"`
@@ -215,11 +209,6 @@ func load() *Config {
if err != nil { if err != nil {
slog.Error("not possible to load env variables for storage : ", err.Error(), "") slog.Error("not possible to load env variables for storage : ", err.Error(), "")
} }
err = loadEnv(&cfg.Log)
if err != nil {
slog.Error("not possible to load env variables for logger : ", err.Error(), "")
}
cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder) cfg.Storage.RootFolder = ResolveRelativePath(cfg.Storage.RootFolder)
return cfg return cfg

View File

@@ -1,6 +1,7 @@
package public package public
import ( import (
"log"
"strconv" "strconv"
"time" "time"
@@ -10,7 +11,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/authService" "git.ma-al.com/goc_daniel/b2b/app/service/authService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -178,13 +178,7 @@ func (h *AuthHandler) ForgotPassword(c fiber.Ctx) error {
// Request password reset - always return success to prevent email enumeration // Request password reset - always return success to prevent email enumeration
err := h.authService.RequestPasswordReset(req.Email) err := h.authService.RequestPasswordReset(req.Email)
if err != nil { if err != nil {
log.Printf("Password reset request error: %v", err)
logger.Warn("password reset request failed",
"handler", "AuthHandler.ForgotPassword",
"email", req.Email,
"error", err.Error(),
)
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
@@ -313,6 +307,7 @@ func (h *AuthHandler) Register(c fiber.Ctx) error {
// Attempt registration // Attempt registration
err := h.authService.Register(&req) err := h.authService.Register(&req)
if err != nil { if err != nil {
log.Printf("Register error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
@@ -452,6 +447,7 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code) response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
if err != nil { if err != nil {
log.Printf("Google OAuth callback error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })

View File

@@ -6,7 +6,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService" "git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -46,13 +45,6 @@ func (h *AddressesHandler) GetTemplate(c fiber.Ctx) error {
template, err := h.addressesService.GetTemplate(uint(country_id)) template, err := h.addressesService.GetTemplate(uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to get address template",
"handler", "AddressesHandler.GetTemplate",
"country_id", country_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -82,13 +74,6 @@ func (h *AddressesHandler) AddNewAddress(c fiber.Ctx) error {
err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id)) err = h.addressesService.AddNewAddress(userID, address_info, uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to add new address",
"handler", "AddressesHandler.AddNewAddress",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -125,14 +110,6 @@ func (h *AddressesHandler) ModifyAddress(c fiber.Ctx) error {
err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id)) err = h.addressesService.ModifyAddress(userID, uint(address_id), address_info, uint(country_id))
if err != nil { if err != nil {
logger.Error("failed to modify address",
"handler", "AddressesHandler.ModifyAddress",
"user_id", userID,
"address_id", address_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -149,13 +126,6 @@ func (h *AddressesHandler) RetrieveAddressesInfo(c fiber.Ctx) error {
addresses, err := h.addressesService.RetrieveAddresses(userID) addresses, err := h.addressesService.RetrieveAddresses(userID)
if err != nil { if err != nil {
logger.Error("failed to retrieve addresses",
"handler", "AddressesHandler.RetrieveAddressesInfo",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -179,14 +149,6 @@ func (h *AddressesHandler) DeleteAddress(c fiber.Ctx) error {
err = h.addressesService.DeleteAddress(userID, uint(address_id)) err = h.addressesService.DeleteAddress(userID, uint(address_id))
if err != nil { if err != nil {
logger.Error("failed to delete address",
"handler", "AddressesHandler.DeleteAddress",
"user_id", userID,
"address_id", address_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -6,7 +6,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/cartsService" "git.ma-al.com/goc_daniel/b2b/app/service/cartsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -29,12 +28,12 @@ func NewCartsHandler() *CartsHandler {
func CartsHandlerRoutes(r fiber.Router) fiber.Router { func CartsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCartsHandler() handler := NewCartsHandler()
r.Post("/add-new-cart", handler.AddNewCart) r.Get("/add-new-cart", handler.AddNewCart)
r.Delete("/remove-cart", handler.RemoveCart) r.Delete("/remove-cart", handler.RemoveCart)
r.Patch("/change-cart-name", handler.ChangeCartName) r.Get("/change-cart-name", handler.ChangeCartName)
r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo) r.Get("/retrieve-carts-info", handler.RetrieveCartsInfo)
r.Get("/retrieve-cart", handler.RetrieveCart) r.Get("/retrieve-cart", handler.RetrieveCart)
r.Post("/add-product-to-cart", handler.AddProduct) r.Get("/add-product-to-cart", handler.AddProduct)
r.Delete("/remove-product-from-cart", handler.RemoveProduct) r.Delete("/remove-product-from-cart", handler.RemoveProduct)
return r return r
@@ -50,13 +49,6 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
name := c.Query("name") name := c.Query("name")
new_cart, err := h.cartsService.CreateNewCart(userID, name) new_cart, err := h.cartsService.CreateNewCart(userID, name)
if err != nil { if err != nil {
logger.Error("failed to create cart",
"handler", "CartsHandler.AddNewCart",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -80,14 +72,6 @@ func (h *CartsHandler) RemoveCart(c fiber.Ctx) error {
err = h.cartsService.RemoveCart(userID, uint(cart_id)) err = h.cartsService.RemoveCart(userID, uint(cart_id))
if err != nil { if err != nil {
logger.Error("failed to remove cart",
"handler", "CartsHandler.RemoveCart",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -113,14 +97,6 @@ func (h *CartsHandler) ChangeCartName(c fiber.Ctx) error {
err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name) err = h.cartsService.UpdateCartName(userID, uint(cart_id), new_name)
if err != nil { if err != nil {
logger.Error("failed to update cart name",
"handler", "CartsHandler.ChangeCartName",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -137,13 +113,6 @@ func (h *CartsHandler) RetrieveCartsInfo(c fiber.Ctx) error {
carts_info, err := h.cartsService.RetrieveCartsInfo(userID) carts_info, err := h.cartsService.RetrieveCartsInfo(userID)
if err != nil { if err != nil {
logger.Error("failed to retrieve carts info",
"handler", "CartsHandler.RetrieveCartsInfo",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -167,14 +136,6 @@ func (h *CartsHandler) RetrieveCart(c fiber.Ctx) error {
cart, err := h.cartsService.RetrieveCart(userID, uint(cart_id)) cart, err := h.cartsService.RetrieveCart(userID, uint(cart_id))
if err != nil { if err != nil {
logger.Error("failed to retrieve cart",
"handler", "CartsHandler.RetrieveCart",
"user_id", userID,
"cart_id", cart_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -234,15 +195,6 @@ func (h *CartsHandler) AddProduct(c fiber.Ctx) error {
err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, amount, set_amount) err = h.cartsService.AddProduct(userID, uint(cart_id), uint(product_id), product_attribute_id, amount, set_amount)
if err != nil { if err != nil {
logger.Error("failed to add product to cart",
"handler", "CartsHandler.AddProduct",
"user_id", userID,
"cart_id", cart_id,
"product_id", product_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -288,15 +240,6 @@ func (h *CartsHandler) RemoveProduct(c fiber.Ctx) error {
err = h.cartsService.RemoveProduct(userID, uint(cart_id), uint(product_id), product_attribute_id) err = h.cartsService.RemoveProduct(userID, uint(cart_id), uint(product_id), product_attribute_id)
if err != nil { if err != nil {
logger.Error("failed to remove product from cart",
"handler", "CartsHandler.RemoveProduct",
"user_id", userID,
"cart_id", cart_id,
"product_id", product_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService" "git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -47,12 +46,6 @@ func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
err := h.CurrencyService.CreateCurrencyRate(&currencyRate) err := h.CurrencyService.CreateCurrencyRate(&currencyRate)
if err != nil { if err != nil {
logger.Error("failed to create currency rate",
"handler", "CurrencyHandler.PostCurrencyRate",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -70,13 +63,6 @@ func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
currency, err := h.CurrencyService.GetCurrency(uint(id)) currency, err := h.CurrencyService.GetCurrency(uint(id))
if err != nil { if err != nil {
logger.Error("failed to get currency",
"handler", "CurrencyHandler.GetCurrencyRate",
"currency_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/customerService" "git.ma-al.com/goc_daniel/b2b/app/service/customerService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -65,13 +64,6 @@ func (h *customerHandler) customerData(fc fiber.Ctx) error {
customer, err := h.service.GetById(customerId) customer, err := h.service.GetById(customerId)
if err != nil { if err != nil {
logger.Error("failed to get customer",
"handler", "customerHandler.customerData",
"customer_id", customerId,
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)). return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
} }
@@ -96,12 +88,6 @@ func (h *customerHandler) listCustomers(fc fiber.Ctx) error {
customer, err := h.service.Find(user.LangID, p, filt, search) customer, err := h.service.Find(user.LangID, p, filt, search)
if err != nil { if err != nil {
logger.Error("failed to list customers",
"handler", "customerHandler.listCustomers",
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)). return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
} }
@@ -134,13 +120,6 @@ func (h *customerHandler) setCustomerNoVatStatus(fc fiber.Ctx) error {
} }
if err := h.service.SetCustomerNoVatStatus(req.CustomerID, req.IsNoVat); err != nil { if err := h.service.SetCustomerNoVatStatus(req.CustomerID, req.IsNoVat); err != nil {
logger.Error("failed to set customer no vat status",
"handler", "customerHandler.setCustomerNoVatStatus",
"customer_id", req.CustomerID,
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(err)). return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
} }

View File

@@ -3,7 +3,6 @@ package restricted
import ( import (
"git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService" "git.ma-al.com/goc_daniel/b2b/app/service/localeSelectorService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -35,12 +34,6 @@ func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error { func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
languages, err := h.localeSelectorService.GetLanguages() languages, err := h.localeSelectorService.GetLanguages()
if err != nil { if err != nil {
logger.Error("failed to get languages",
"handler", "LocaleSelectorHandler.GetLanguages",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -51,12 +44,6 @@ func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error { func (h *LocaleSelectorHandler) GetCountries(c fiber.Ctx) error {
countries, err := h.localeSelectorService.GetCountriesAndCurrencies() countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
if err != nil { if err != nil {
logger.Error("failed to get countries",
"handler", "LocaleSelectorHandler.GetCountries",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -3,12 +3,9 @@ package restricted
import ( import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -32,7 +29,6 @@ func MenuHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/get-category-tree", handler.GetCategoryTree) r.Get("/get-category-tree", handler.GetCategoryTree)
r.Get("/get-breadcrumb", handler.GetBreadcrumb) r.Get("/get-breadcrumb", handler.GetBreadcrumb)
r.Get("/get-top-menu", handler.GetTopMenu) r.Get("/get-top-menu", handler.GetTopMenu)
r.Get("/get-customer-management-menu", middleware.Require(perms.UserReadAny), handler.GetCustomerManagementMenu)
return r return r
} }
@@ -53,12 +49,6 @@ func (h *MenuHandler) GetCategoryTree(c fiber.Ctx) error {
category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id) category_tree, err := h.menuService.GetCategoryTree(uint(root_category_id), lang_id)
if err != nil { if err != nil {
logger.Error("failed to get category tree",
"handler", "MenuHandler.GetCategoryTree",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -89,12 +79,6 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id) breadcrumb, err := h.menuService.GetBreadcrumb(uint(root_category_id), uint(category_id), lang_id)
if err != nil { if err != nil {
logger.Error("failed to get breadcrumb",
"handler", "MenuHandler.GetBreadcrumb",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -109,27 +93,6 @@ func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
menu, err := h.menuService.GetTopMenu(customer.LangID, customer.RoleID) menu, err := h.menuService.GetTopMenu(customer.LangID, customer.RoleID)
if err != nil {
logger.Error("failed to get top menu",
"handler", "MenuHandler.GetTopMenu",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&menu, len(menu), i18n.T_(c, response.Message_OK)))
}
func (h *MenuHandler) GetCustomerManagementMenu(c fiber.Ctx) error {
langId, ok := localeExtractor.GetLangID(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
menu, err := h.menuService.GetCustomerManagementMenu(langId)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -2,16 +2,11 @@ package restricted
import ( import (
"strconv" "strconv"
"strings"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
"git.ma-al.com/goc_daniel/b2b/app/service/orderService" "git.ma-al.com/goc_daniel/b2b/app/service/orderService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -36,7 +31,7 @@ func OrdersHandlerRoutes(r fiber.Router) fiber.Router {
r.Get("/list", handler.ListOrders) r.Get("/list", handler.ListOrders)
r.Post("/place-new-order", handler.PlaceNewOrder) r.Post("/place-new-order", handler.PlaceNewOrder)
r.Post("/change-order-address", handler.ChangeOrderAddress) r.Post("/change-order-address", handler.ChangeOrderAddress)
r.Patch("/change-order-status", middleware.Require(perms.OrdersModifyAll), handler.ChangeOrderStatus) r.Get("/change-order-status", handler.ChangeOrderStatus)
return r return r
} }
@@ -58,12 +53,6 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error {
list, err := h.ordersService.Find(user, paging, filters) list, err := h.ordersService.Find(user, paging, filters)
if err != nil { if err != nil {
logger.Error("failed to list orders",
"handler", "OrdersHandler.ListOrders",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -77,11 +66,6 @@ var columnMappingListOrders map[string]string = map[string]string{
"name": "b2b_customer_orders.name", "name": "b2b_customer_orders.name",
"country_id": "b2b_customer_orders.country_id", "country_id": "b2b_customer_orders.country_id",
"status": "b2b_customer_orders.status", "status": "b2b_customer_orders.status",
"base_price": "b2b_customer_orders.base_price",
"tax_incl": "b2b_customer_orders.tax_incl",
"tax_excl": "b2b_customer_orders.tax_excl",
"created_at": "b2b_customer_orders.created_at",
"updated_at": "b2b_customer_orders.updated_at",
} }
func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error { func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
@@ -113,21 +97,8 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
name := c.Query("name") name := c.Query("name")
originalUserId, ok := localeExtractor.GetOriginalUserID(c) err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
}
err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info, originalUserId)
if err != nil { if err != nil {
logger.Error("failed to place order",
"handler", "OrdersHandler.PlaceNewOrder",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -165,13 +136,6 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error {
err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info) err = h.ordersService.ChangeOrderAddress(user, uint(order_id), uint(country_id), address_info)
if err != nil { if err != nil {
logger.Error("failed to change order address",
"handler", "OrdersHandler.ChangeOrderAddress",
"order_id", order_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -182,27 +146,23 @@ func (h *OrdersHandler) ChangeOrderAddress(c fiber.Ctx) error {
// we base permissions and user based on target user only. // we base permissions and user based on target user only.
// TODO: well, permissions and all that. // TODO: well, permissions and all that.
func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error { func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error {
userId, ok := localeExtractor.GetUserID(c) user, ok := localeExtractor.GetCustomer(c)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
} }
order_id, err := strconv.Atoi(c.Query("order_id")) order_id_attribute := c.Query("order_id")
order_id, err := strconv.Atoi(order_id_attribute)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
err = h.ordersService.ChangeOrderStatus(userId, uint(order_id), enums.OrderStatus(strings.ToUpper(c.Query("status")))) status := c.Query("status")
err = h.ordersService.ChangeOrderStatus(user, uint(order_id), status)
if err != nil { if err != nil {
logger.Error("failed to change order status",
"handler", "OrdersHandler.ChangeOrderStatus",
"order_id", order_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params" "git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
@@ -75,15 +74,6 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
} }
productJson, err := h.productService.Get(uint(p_id_product), customer.LangID, customer.ID, uint(b2b_id_country), uint(p_quantity)) productJson, err := h.productService.Get(uint(p_id_product), customer.LangID, customer.ID, uint(b2b_id_country), uint(p_quantity))
if err != nil { if err != nil {
logger.Error("failed to get product",
"handler", "ProductsHandler.GetProductJson",
"product_id", p_id_product,
"lang_id", customer.LangID,
"customer_id", customer.ID,
"b2b_id_country", b2b_id_country,
"quantity", p_quantity,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -106,13 +96,6 @@ func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID) list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID)
if err != nil { if err != nil {
logger.Error("failed to list products",
"handler", "ProductsHandler.ListProducts",
"lang_id", customer.LangID,
"customer_id", customer.ID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -149,13 +132,6 @@ func (h *ProductsHandler) AddToFavorites(c fiber.Ctx) error {
err = h.productService.AddToFavorites(userID, uint(productID)) err = h.productService.AddToFavorites(userID, uint(productID))
if err != nil { if err != nil {
logger.Error("failed to add to favorites",
"handler", "ProductsHandler.AddToFavorites",
"user_id", userID,
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -180,12 +156,6 @@ func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
err = h.productService.RemoveFromFavorites(userID, uint(productID)) err = h.productService.RemoveFromFavorites(userID, uint(productID))
if err != nil { if err != nil {
logger.Error("failed to remove from favorites",
"handler", "ProductsHandler.RemoveFromFavorites",
"user_id", userID,
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -210,15 +180,6 @@ func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_QUANTITY) list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_QUANTITY)
if err != nil { if err != nil {
logger.Error("failed to list product variants",
"handler", "ProductsHandler.ListProductVariants",
"product_id", productID,
"customer_id", customer.ID,
"lang_id", customer.LangID,
"country_id", customer.CountryID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService" "git.ma-al.com/goc_daniel/b2b/app/service/productTranslationService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -67,13 +66,6 @@ func (h *ProductTranslationHandler) GetProductDescription(c fiber.Ctx) error {
description, err := h.productTranslationService.GetProductDescription(userID, uint(productID), uint(productLangID)) description, err := h.productTranslationService.GetProductDescription(userID, uint(productID), uint(productLangID))
if err != nil { if err != nil {
logger.Error("failed to get product description",
"handler", "ProductTranslationHandler.GetProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -111,13 +103,6 @@ func (h *ProductTranslationHandler) SaveProductDescription(c fiber.Ctx) error {
err = h.productTranslationService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates) err = h.productTranslationService.SaveProductDescription(userID, uint(productID), uint(productLangID), updates)
if err != nil { if err != nil {
logger.Error("failed to save product description",
"handler", "ProductTranslationHandler.SaveProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -162,13 +147,6 @@ func (h *ProductTranslationHandler) TranslateProductDescription(c fiber.Ctx) err
description, err := h.productTranslationService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel) description, err := h.productTranslationService.TranslateProductDescription(userID, uint(productID), uint(productFromLangID), uint(productToLangID), aiModel)
if err != nil { if err != nil {
logger.Error("failed to translate product description",
"handler", "ProductTranslationHandler.TranslateProductDescription",
"product_id", productID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -2,6 +2,7 @@ package restricted
import ( import (
"encoding/json" "encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
@@ -9,7 +10,6 @@ import (
searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService" searchservice "git.ma-al.com/goc_daniel/b2b/app/service/searchService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -47,13 +47,7 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
err := h.meiliService.CreateIndex(id_lang) err := h.meiliService.CreateIndex(id_lang)
if err != nil { if err != nil {
fmt.Printf("CreateIndex error: %v\n", err)
logger.Error("failed to create search index",
"handler", "MeiliSearchHandler.CreateIndex",
"lang_id", id_lang,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -78,13 +72,6 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
result, err := h.searchService.Search(index, c.Body(), id_lang) result, err := h.searchService.Search(index, c.Body(), id_lang)
if err != nil { if err != nil {
logger.Error("failed to search",
"handler", "MeiliSearchHandler.Search",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -93,13 +80,6 @@ func (h *MeiliSearchHandler) Search(c fiber.Ctx) error {
if createErr := h.meiliService.CreateIndex(id_lang); createErr == nil { if createErr := h.meiliService.CreateIndex(id_lang); createErr == nil {
result, err = h.searchService.Search(index, c.Body(), id_lang) result, err = h.searchService.Search(index, c.Body(), id_lang)
if err != nil { if err != nil {
logger.Error("failed to search after index creation",
"handler", "MeiliSearchHandler.Search",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -120,13 +100,6 @@ func (h *MeiliSearchHandler) GetSettings(c fiber.Ctx) error {
result, err := h.searchService.GetIndexSettings(index) result, err := h.searchService.GetIndexSettings(index)
if err != nil { if err != nil {
logger.Error("failed to get index settings",
"handler", "MeiliSearchHandler.GetSettings",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService" "git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -52,12 +51,6 @@ func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
result, err := h.SpecificPriceService.Create(c.Context(), &pr) result, err := h.SpecificPriceService.Create(c.Context(), &pr)
if err != nil { if err != nil {
logger.Error("failed to create specific price",
"handler", "SpecificPriceHandler.Create",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -81,13 +74,6 @@ func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
result, err := h.SpecificPriceService.Update(c.Context(), id, &pr) result, err := h.SpecificPriceService.Update(c.Context(), id, &pr)
if err != nil { if err != nil {
logger.Error("failed to update specific price",
"handler", "SpecificPriceHandler.Update",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -98,12 +84,6 @@ func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
func (h *SpecificPriceHandler) List(c fiber.Ctx) error { func (h *SpecificPriceHandler) List(c fiber.Ctx) error {
result, err := h.SpecificPriceService.List(c.Context()) result, err := h.SpecificPriceService.List(c.Context())
if err != nil { if err != nil {
logger.Error("failed to list specific prices",
"handler", "SpecificPriceHandler.List",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -121,13 +101,6 @@ func (h *SpecificPriceHandler) GetByID(c fiber.Ctx) error {
result, err := h.SpecificPriceService.GetByID(c.Context(), id) result, err := h.SpecificPriceService.GetByID(c.Context(), id)
if err != nil { if err != nil {
logger.Error("failed to get specific price",
"handler", "SpecificPriceHandler.GetByID",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -145,13 +118,6 @@ func (h *SpecificPriceHandler) Activate(c fiber.Ctx) error {
err = h.SpecificPriceService.SetActive(c.Context(), id, true) err = h.SpecificPriceService.SetActive(c.Context(), id, true)
if err != nil { if err != nil {
logger.Error("failed to activate specific price",
"handler", "SpecificPriceHandler.Activate",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -169,13 +135,6 @@ func (h *SpecificPriceHandler) Deactivate(c fiber.Ctx) error {
err = h.SpecificPriceService.SetActive(c.Context(), id, false) err = h.SpecificPriceService.SetActive(c.Context(), id, false)
if err != nil { if err != nil {
logger.Error("failed to deactivate specific price",
"handler", "SpecificPriceHandler.Deactivate",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -193,13 +152,6 @@ func (h *SpecificPriceHandler) Delete(c fiber.Ctx) error {
err = h.SpecificPriceService.Delete(c.Context(), id) err = h.SpecificPriceService.Delete(c.Context(), id)
if err != nil { if err != nil {
logger.Error("failed to delete specific price",
"handler", "SpecificPriceHandler.Delete",
"specific_price_id", id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -9,7 +9,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/storageService" "git.ma-al.com/goc_daniel/b2b/app/service/storageService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor" "git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response" "git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -53,12 +52,6 @@ func (h *StorageHandler) ListContent(c fiber.Ctx) error {
entries_in_list, err := h.storageService.ListContent(abs_path) entries_in_list, err := h.storageService.ListContent(abs_path)
if err != nil { if err != nil {
logger.Error("failed to list storage content",
"handler", "StorageHandler.ListContent",
"path", abs_path,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -75,12 +68,6 @@ func (h *StorageHandler) DownloadFile(c fiber.Ctx) error {
f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path) f, filename, filesize, err := h.storageService.DownloadFilePrep(abs_path)
if err != nil { if err != nil {
logger.Error("failed to prepare file download",
"handler", "StorageHandler.DownloadFile",
"path", abs_path,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }
@@ -100,12 +87,6 @@ func (h *StorageHandler) CreateNewWebdavToken(c fiber.Ctx) error {
new_token, err := h.storageService.NewWebdavToken(userID) new_token, err := h.storageService.NewWebdavToken(userID)
if err != nil { if err != nil {
logger.Error("failed to create webdav token",
"handler", "StorageHandler.CreateNewWebdavToken",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
} }

View File

@@ -1,16 +0,0 @@
package enums
type OrderStatus string
const (
OrderStatusPending OrderStatus = "PENDING"
OrderStatusConfirmed OrderStatus = "CONFIRMED"
OrderStatusProcessing OrderStatus = "PROCESSING"
OrderStatusShipped OrderStatus = "SHIPPED"
OrderStatusOutForDelivery OrderStatus = "OUT_FOR_DELIVERY"
OrderStatusDelivered OrderStatus = "DELIVERED"
OrderStatusCancelled OrderStatus = "CANCELLED"
OrderStatusReturned OrderStatus = "RETURNED"
OrderStatusRefunded OrderStatus = "REFUNDED"
OrderStatusFailed OrderStatus = "FAILED"
)

View File

@@ -1,11 +1,5 @@
package model package model
import (
"time"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
)
type CustomerOrder struct { type CustomerOrder struct {
OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"` OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_id"`
UserID uint `gorm:"column:user_id;not null;index" json:"user_id"` UserID uint `gorm:"column:user_id;not null;index" json:"user_id"`
@@ -13,12 +7,7 @@ type CustomerOrder struct {
CountryID uint `gorm:"column:country_id;not null" json:"country_id"` CountryID uint `gorm:"column:country_id;not null" json:"country_id"`
AddressString string `gorm:"column:address_string;not null" json:"address_string"` AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"` AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
Status enums.OrderStatus `gorm:"column:status;size:50;not null" json:"status"` Status string `gorm:"column:status;size:50;not null" json:"status"`
BasePrice float64 `gorm:"column:base_price;type:decimal(10,2);not null" json:"base_price"`
TaxIncl float64 `gorm:"column:tax_incl;type:decimal(10,2);not null" json:"tax_incl"`
TaxExcl float64 `gorm:"column:tax_excl;type:decimal(10,2);not null" json:"tax_excl"`
CreatedAt time.Time `gorm:"column:created_at;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null" json:"updated_at"`
Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"` Products []OrderProduct `gorm:"foreignKey:OrderID;references:OrderID" json:"products"`
} }

View File

@@ -1,20 +0,0 @@
package model
import (
"time"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
)
type OrderStatusHistory struct {
Id uint `gorm:"column:id;primaryKey;autoIncrement"`
OrderId uint `gorm:"column:order_id;not null;index:idx_order_status_history_order"`
OldStatus *enums.OrderStatus `gorm:"column:old_status;type:varchar(50)"`
NewStatus enums.OrderStatus `gorm:"column:new_status;type:varchar(50);not null"`
CreatedAt time.Time `gorm:"column:created_at;not null;autoCreateTime"`
UserId uint `gorm:"column:user_id;index:idx_order_status_history_user;not null"`
}
func (OrderStatusHistory) TableName() string {
return "b2b_order_status_history"
}

View File

@@ -2,7 +2,7 @@ package model
import "encoding/json" import "encoding/json"
type B2BMenu struct { type B2BTopMenu struct {
MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"` MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"`
Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"` Label json.RawMessage `gorm:"column:label;type:longtext;not null;default:'{}'" json:"label"`
ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"` ParentID *int `gorm:"column:parent_id;index:FK_b2b_top_menu_parent_id" json:"parent_id,omitempty"`
@@ -10,22 +10,10 @@ type B2BMenu struct {
Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"` Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"`
Position int `gorm:"column:position;not null;default:1" json:"position"` Position int `gorm:"column:position;not null;default:1" json:"position"`
Parent *B2BMenu `gorm:"foreignKey:ParentID;references:MenuID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"parent,omitempty"` Parent *B2BTopMenu `gorm:"foreignKey:ParentID;references:MenuID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"parent,omitempty"`
Children []*B2BMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"` Children []*B2BTopMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
type B2BTopMenu struct {
B2BMenu
} }
func (B2BTopMenu) TableName() string { func (B2BTopMenu) TableName() string {
return "b2b_top_menu" return "b2b_top_menu"
} }
type B2BCustomerManagementMenu struct {
B2BMenu
}
func (B2BCustomerManagementMenu) TableName() string {
return "b2b_customer_management_menu"
}

View File

@@ -1,23 +1,19 @@
package ordersRepo package ordersRepo
import ( import (
"time"
"git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/enums" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
) )
type UIOrdersRepo interface { type UIOrdersRepo interface {
UserHasOrder(user_id uint, order_id uint) (bool, error) UserHasOrder(user_id uint, order_id uint) (bool, error)
Get(orderId uint) (*model.CustomerOrder, error)
Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error)
PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string, originalUserId uint, base_price float64, tax_incl float64, tax_excl float64) (*model.CustomerOrder, error) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error
ChangeOrderAddress(order_id uint, country_id uint, address_info string) error ChangeOrderAddress(order_id uint, country_id uint, address_info string) error
ChangeOrderStatus(orderId uint, newStatus enums.OrderStatus, userId uint) error ChangeOrderStatus(order_id uint, status string) error
GetOrderStatus(orderID uint) (enums.OrderStatus, error)
} }
type OrdersRepo struct{} type OrdersRepo struct{}
@@ -39,18 +35,6 @@ func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error)
return amt >= 1, err return amt >= 1, err
} }
func (repo *OrdersRepo) Get(orderId uint) (*model.CustomerOrder, error) {
var order model.CustomerOrder
err := db.Get().
Model(&model.CustomerOrder{}).
Preload("Products").
Where("order_id = ?", orderId).
First(&order).Error
return &order, err
}
func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) { func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
var list []model.CustomerOrder var list []model.CustomerOrder
var total int64 var total int64
@@ -85,13 +69,13 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL
}, nil }, nil
} }
func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string, originalUserId uint, base_price float64, tax_incl float64, tax_excl float64) (*model.CustomerOrder, error) { func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, country_id uint, address_info string) error {
order := model.CustomerOrder{ order := model.CustomerOrder{
UserID: cart.UserID, UserID: cart.UserID,
Name: name, Name: name,
CountryID: country_id, CountryID: country_id,
AddressString: address_info, AddressString: address_info,
Status: enums.OrderStatusPending, Status: constdata.NEW_ORDER_STATUS,
Products: make([]model.OrderProduct, 0, len(cart.Products)), Products: make([]model.OrderProduct, 0, len(cart.Products)),
} }
@@ -102,35 +86,8 @@ func (repo *OrdersRepo) PlaceNewOrder(cart *model.CustomerCart, name string, cou
Amount: product.Amount, Amount: product.Amount,
}) })
} }
order.CreatedAt = time.Now()
order.UpdatedAt = time.Now()
order.BasePrice = base_price
order.TaxIncl = tax_incl
order.TaxExcl = tax_excl
tx := db.Get().Begin()
err := tx.Create(&order).Error
if err != nil {
tx.Rollback()
return nil, err
}
history := model.OrderStatusHistory{
OrderId: order.OrderID,
OldStatus: nil,
NewStatus: enums.OrderStatusPending,
UserId: originalUserId,
}
err = tx.Create(&history).Error return db.DB.Create(&order).Error
if err != nil {
tx.Rollback()
return nil, err
}
err = tx.Commit().Error
if err != nil {
return nil, err
}
return &order, nil
} }
func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error { func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, address_info string) error {
@@ -140,53 +97,14 @@ func (repo *OrdersRepo) ChangeOrderAddress(order_id uint, country_id uint, addre
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"country_id": country_id, "country_id": country_id,
"address_string": address_info, "address_string": address_info,
"updated_at": time.Now(),
}). }).
Error Error
} }
func (repo *OrdersRepo) ChangeOrderStatus(orderID uint, newStatus enums.OrderStatus, userId uint) error { func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error {
tx := db.Get().Begin() return db.DB.
Table("b2b_customer_orders").
var currentStatus enums.OrderStatus Where("order_id = ?", order_id).
err := tx.Table("b2b_customer_orders"). Update("status", status).
Select("status"). Error
Where("order_id = ?", orderID).
Scan(&currentStatus).Error
if err != nil {
tx.Rollback()
return err
}
err = tx.Table("b2b_customer_orders").
Where("order_id = ?", orderID).
Update("status", string(newStatus)).Error
if err != nil {
tx.Rollback()
return err
}
history := model.OrderStatusHistory{
OrderId: orderID,
OldStatus: &currentStatus,
NewStatus: newStatus,
UserId: userId,
}
err = tx.Create(&history).Error
if err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
func (repo *OrdersRepo) GetOrderStatus(orderID uint) (enums.OrderStatus, error) {
var status enums.OrderStatus
err := db.DB.Table("b2b_customer_orders").
Select("status").
Where("order_id = ?", orderID).
Scan(&status).Error
return status, err
} }

View File

@@ -9,7 +9,6 @@ import (
type UIRoutesRepo interface { type UIRoutesRepo interface {
GetRoutes(langId uint, roleId uint) ([]model.Route, error) GetRoutes(langId uint, roleId uint) ([]model.Route, error)
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error) GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
GetCustomerManagementMenu(langId uint) ([]model.B2BCustomerManagementMenu, error)
} }
type RoutesRepo struct{} type RoutesRepo struct{}
@@ -39,23 +38,10 @@ func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, e
Get(). Get().
Model(model.B2BTopMenu{}). Model(model.B2BTopMenu{}).
Joins("JOIN b2b_top_menu_roles tmr ON tmr.top_menu_id = b2b_top_menu.menu_id"). Joins("JOIN b2b_top_menu_roles tmr ON tmr.top_menu_id = b2b_top_menu.menu_id").
Where(model.B2BTopMenu{B2BMenu: model.B2BMenu{Active: 1}}). Where(model.B2BTopMenu{Active: 1}).
Where("tmr.role_id = ?", roleId). Where("tmr.role_id = ?", roleId).
Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC"). Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC").
Find(&menus).Error Find(&menus).Error
return menus, err return menus, err
} }
func (p *RoutesRepo) GetCustomerManagementMenu(langId uint) ([]model.B2BCustomerManagementMenu, error) {
var menus []model.B2BCustomerManagementMenu
err := db.
Get().
Model(model.B2BCustomerManagementMenu{}).
Where(model.B2BCustomerManagementMenu{B2BMenu: model.B2BMenu{Active: 1}}).
Order("b2b_customer_management_menu.parent_id ASC, b2b_customer_management_menu.position ASC").
Find(&menus).Error
return menus, err
}

View File

@@ -15,7 +15,6 @@ import (
roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo" roleRepo "git.ma-al.com/goc_daniel/b2b/app/repos/rolesRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService" "git.ma-al.com/goc_daniel/b2b/app/service/emailService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
@@ -69,47 +68,22 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
// Find user by email // Find user by email
if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil { if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Info("login failed - invalid credentials",
"service", "AuthService.Login",
"email", req.Email,
"reason", "user not found",
)
return nil, "", responseErrors.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
logger.Error("login failed - database error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("database error: %w", err) return nil, "", fmt.Errorf("database error: %w", err)
} }
// Check if user is active // Check if user is active
if !user.IsActive { if !user.IsActive {
logger.Info("login failed - user inactive",
"service", "AuthService.Login",
"email", req.Email,
"reason", "user account is inactive",
)
return nil, "", responseErrors.ErrUserInactive return nil, "", responseErrors.ErrUserInactive
} }
// Check if email is verified // Check if email is verified
if !user.EmailVerified { if !user.EmailVerified {
logger.Info("login failed - email not verified",
"service", "AuthService.Login",
"email", req.Email,
"reason", "email not verified",
)
return nil, "", responseErrors.ErrEmailNotVerified return nil, "", responseErrors.ErrEmailNotVerified
} }
// Verify password // Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
logger.Info("login failed - invalid credentials",
"service", "AuthService.Login",
"email", req.Email,
"reason", "wrong password",
)
return nil, "", responseErrors.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
@@ -120,38 +94,22 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
if req.LangID != nil { if req.LangID != nil {
_, err := s.GetLangISOCode(*req.LangID) _, err := s.GetLangISOCode(*req.LangID)
if err != nil { if err != nil {
logger.Warn("login failed - invalid language ID",
"service", "AuthService.Login",
"email", req.Email,
"reason", "invalid language ID",
)
return nil, "", responseErrors.ErrBadLangID return nil, "", responseErrors.ErrBadLangID
} }
user.LangID = *req.LangID user.LangID = *req.LangID
} }
user.Country = nil
s.db.Save(&user) s.db.Save(&user)
// Generate access token (JWT) // Generate access token (JWT)
accessToken, err := s.generateAccessToken(&user) accessToken, err := s.generateAccessToken(&user)
if err != nil { if err != nil {
logger.Error("login failed - token generation error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("failed to generate access token: %w", err) return nil, "", fmt.Errorf("failed to generate access token: %w", err)
} }
// Generate opaque refresh token and store in DB // Generate opaque refresh token and store in DB
rawRefreshToken, err := s.createRefreshToken(user.ID) rawRefreshToken, err := s.createRefreshToken(user.ID)
if err != nil { if err != nil {
logger.Error("login failed - refresh token creation error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("failed to create refresh token: %w", err) return nil, "", fmt.Errorf("failed to create refresh token: %w", err)
} }
@@ -212,11 +170,6 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
} }
if err := s.db.Create(&user).Error; err != nil { if err := s.db.Create(&user).Error; err != nil {
logger.Error("registration failed - database error",
"service", "AuthService.Register",
"email", req.Email,
"error", err.Error(),
)
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)
} }
@@ -228,11 +181,8 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
} }
if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil { if err := s.email.SendVerificationEmail(user.Email, user.EmailVerificationToken, baseURL, lang); err != nil {
logger.Warn("failed to send verification email", // Log error but don't fail registration - user can request resend
"service", "AuthService.Register", _ = err
"email", req.Email,
"error", err.Error(),
)
} }
return nil return nil
@@ -260,7 +210,6 @@ func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationReques
user.EmailVerificationToken = "" user.EmailVerificationToken = ""
user.EmailVerificationExpires = nil user.EmailVerificationExpires = nil
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
return nil, "", fmt.Errorf("failed to update user: %w", err) return nil, "", fmt.Errorf("failed to update user: %w", err)
} }
@@ -329,7 +278,6 @@ func (s *AuthService) RequestPasswordReset(emailAddr string) error {
user.PasswordResetToken = token user.PasswordResetToken = token
user.PasswordResetExpires = &expiresAt user.PasswordResetExpires = &expiresAt
user.LastPasswordResetRequest = &now user.LastPasswordResetRequest = &now
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
return fmt.Errorf("failed to save reset token: %w", err) return fmt.Errorf("failed to save reset token: %w", err)
} }
@@ -356,10 +304,6 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return responseErrors.ErrInvalidResetToken return responseErrors.ErrInvalidResetToken
} }
logger.Error("password reset failed - database error",
"service", "AuthService.ResetPassword",
"error", err.Error(),
)
return fmt.Errorf("database error: %w", err) return fmt.Errorf("database error: %w", err)
} }
@@ -384,12 +328,7 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
user.PasswordResetToken = "" user.PasswordResetToken = ""
user.PasswordResetExpires = nil user.PasswordResetExpires = nil
user.Country = nil
if err := s.db.Save(&user).Error; err != nil { if err := s.db.Save(&user).Error; err != nil {
logger.Error("password reset failed - database error",
"service", "AuthService.ResetPassword",
"error", err.Error(),
)
return fmt.Errorf("failed to update password: %w", err) return fmt.Errorf("failed to update password: %w", err)
} }
@@ -600,7 +539,6 @@ func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
} }
// Save the updated user // Save the updated user
user.Country = nil
if err := s.db.Save(user).Error; err != nil { if err := s.db.Save(user).Error; err != nil {
return "", fmt.Errorf("database error: %w", err) return "", fmt.Errorf("database error: %w", err)
} }

View File

@@ -8,12 +8,10 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/config" "git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view" "git.ma-al.com/goc_daniel/b2b/app/view"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -79,20 +77,12 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
// Find or create user // Find or create user
user, err := s.findOrCreateGoogleUser(userInfo) user, err := s.findOrCreateGoogleUser(userInfo)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "database") {
logger.Error("google oauth callback failed - database error",
"service", "AuthService.HandleGoogleCallback",
"email", userInfo.Email,
"error", err.Error(),
)
}
return nil, "", err return nil, "", err
} }
// Update last login // Update last login
now := time.Now() now := time.Now()
user.LastLoginAt = &now user.LastLoginAt = &now
user.Country = nil
s.db.Save(user) s.db.Save(user)
// Generate access token (JWT) // Generate access token (JWT)

View File

@@ -4,7 +4,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
@@ -35,15 +34,6 @@ func (s *CartsService) CreateNewCart(user_id uint, name string) (model.CustomerC
// create new cart for customer // create new cart for customer
cart, err = s.repo.CreateNewCart(user_id, name) cart, err = s.repo.CreateNewCart(user_id, name)
if err != nil {
return cart, err
}
logger.Info("cart created",
"service", "cartsService",
"user_id", user_id,
"cart_id", cart.CartID,
)
return cart, nil return cart, nil
} }

View File

@@ -12,7 +12,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/templ/emails" "git.ma-al.com/goc_daniel/b2b/app/templ/emails"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/view" "git.ma-al.com/goc_daniel/b2b/app/view"
) )
@@ -45,11 +44,6 @@ func getLangID(isoCode string) uint {
// SendEmail sends an email to the specified recipient // SendEmail sends an email to the specified recipient
func (s *EmailService) SendEmail(to, subject, body string) error { func (s *EmailService) SendEmail(to, subject, body string) error {
if !s.config.Enabled { if !s.config.Enabled {
logger.Debug("email service is disabled",
"service", "EmailService.SendEmail",
"to", to,
"subject", subject,
)
return fmt.Errorf("email service is disabled") return fmt.Errorf("email service is disabled")
} }
@@ -75,12 +69,6 @@ func (s *EmailService) SendEmail(to, subject, body string) error {
// Send email // Send email
addr := fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort) addr := fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort)
if err := smtp.SendMail(addr, auth, s.config.FromEmail, []string{to}, []byte(msg.String())); err != nil { if err := smtp.SendMail(addr, auth, s.config.FromEmail, []string{to}, []byte(msg.String())); err != nil {
logger.Error("failed to send email",
"service", "EmailService.SendEmail",
"to", to,
"subject", subject,
"error", err.Error(),
)
return fmt.Errorf("failed to send email: %w", err) return fmt.Errorf("failed to send email: %w", err)
} }
@@ -132,12 +120,9 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL
// SendNewOrderPlacedNotification sends an email to admin when new order is placed // SendNewOrderPlacedNotification sends an email to admin when new order is placed
func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error { func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error {
if s.config.AdminEmail == "" { if s.config.AdminEmail == "" {
logger.Warn("no admin email setup in the config", return nil // No admin email configured
"service", "EmailService.SendNewOrderPlacedNotification",
"user_id", userID,
)
return nil
} }
subject := "New Order Created" subject := "New Order Created"
body := s.newOrderPlacedTemplate(userID) body := s.newOrderPlacedTemplate(userID)

View File

@@ -10,7 +10,6 @@ import (
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo" routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
) )
@@ -194,52 +193,18 @@ func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uin
return breadcrumb, nil return breadcrumb, nil
} }
func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BMenu, error) { func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopMenu, error) {
items, err := s.routesRepo.GetTopMenu(languageId, roleId) items, err := s.routesRepo.GetTopMenu(languageId, roleId)
if err != nil { if err != nil {
logger.Error("failed to get top menu",
"handler", "ManuService.GetTopMenu",
"language_id", languageId,
"role_id", roleId,
"error", err.Error(),
)
return nil, err return nil, err
} }
menus := make([]model.B2BMenu, len(items)) menuMap := make(map[int]*model.B2BTopMenu, len(items))
for i := range items { roots := make([]*model.B2BTopMenu, 0)
menus[i] = items[i].B2BMenu
}
return buildMenu(menus), nil
}
func (s *MenuService) GetCustomerManagementMenu(languageId uint) ([]*model.B2BMenu, error) {
items, err := s.routesRepo.GetCustomerManagementMenu(languageId)
if err != nil {
logger.Error("failed to get customer management menu",
"handler", "ManuService.GetCustomerManagementMenu",
"language_id", languageId,
"error", err.Error(),
)
return nil, err
}
menus := make([]model.B2BMenu, len(items))
for i := range items {
menus[i] = items[i].B2BMenu
}
return buildMenu(menus), nil
}
func buildMenu(items []model.B2BMenu) []*model.B2BMenu {
menuMap := make(map[int]*model.B2BMenu, len(items))
roots := make([]*model.B2BMenu, 0)
for i := range items { for i := range items {
menu := &items[i] menu := &items[i]
menu.Children = make([]*model.B2BMenu, 0) menu.Children = make([]*model.B2BTopMenu, 0)
menuMap[menu.MenuID] = menu menuMap[menu.MenuID] = menu
} }
@@ -261,7 +226,7 @@ func buildMenu(items []model.B2BMenu) []*model.B2BMenu {
parent.Children = append(parent.Children, menu) parent.Children = append(parent.Children, menu)
} }
return roots return roots, nil
} }
func (s *MenuService) appendAdditional(all_categories *[]model.ScannedCategory, id_lang uint, iso_code string) { func (s *MenuService) appendAdditional(all_categories *[]model.ScannedCategory, id_lang uint, iso_code string) {

View File

@@ -1,19 +1,15 @@
package orderService package orderService
import ( import (
"fmt"
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/actions/orderStatusActions"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms" "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
"git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/cartsRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo" "git.ma-al.com/goc_daniel/b2b/app/repos/ordersRepo"
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
"git.ma-al.com/goc_daniel/b2b/app/service/addressesService" "git.ma-al.com/goc_daniel/b2b/app/service/addressesService"
"git.ma-al.com/goc_daniel/b2b/app/service/emailService" "git.ma-al.com/goc_daniel/b2b/app/service/emailService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find" "git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -22,36 +18,19 @@ import (
type OrderService struct { type OrderService struct {
ordersRepo ordersRepo.UIOrdersRepo ordersRepo ordersRepo.UIOrdersRepo
cartsRepo cartsRepo.UICartsRepo cartsRepo cartsRepo.UICartsRepo
productsRepo productsRepo.UIProductsRepo
addressesService *addressesService.AddressesService addressesService *addressesService.AddressesService
emailService *emailService.EmailService emailService *emailService.EmailService
actionRegistry *orderStatusActions.ActionRegistry
} }
func New() *OrderService { func New() *OrderService {
return &OrderService{ return &OrderService{
ordersRepo: ordersRepo.New(), ordersRepo: ordersRepo.New(),
cartsRepo: cartsRepo.New(), cartsRepo: cartsRepo.New(),
productsRepo: productsRepo.New(),
addressesService: addressesService.New(), addressesService: addressesService.New(),
emailService: emailService.NewEmailService(), emailService: emailService.NewEmailService(),
actionRegistry: &orderStatusActions.GlobalRegistry,
} }
} }
var ValidStatuses = map[enums.OrderStatus]bool{
enums.OrderStatusPending: true,
enums.OrderStatusConfirmed: true,
enums.OrderStatusProcessing: true,
enums.OrderStatusShipped: true,
enums.OrderStatusOutForDelivery: true,
enums.OrderStatusDelivered: true,
enums.OrderStatusCancelled: true,
enums.OrderStatusReturned: true,
enums.OrderStatusRefunded: true,
enums.OrderStatusFailed: true,
}
func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) { func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.FiltersList) (*find.Found[model.CustomerOrder], error) {
if !user.HasPermission(perms.OrdersViewAll) { if !user.HasPermission(perms.OrdersViewAll) {
// append filter to view only this user's orders // append filter to view only this user's orders
@@ -66,12 +45,9 @@ func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.F
for i := 0; i < len(list.Items); i++ { for i := 0; i < len(list.Items); i++ {
address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID) address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID)
// log such errors
if err != nil { if err != nil {
logger.Warn("failed to validate address", fmt.Printf("err: %v\n", err)
"service", "orderService",
"order_id", list.Items[i].OrderID,
"error", err.Error(),
)
} }
list.Items[i].AddressUnparsed = &address_unparsed list.Items[i].AddressUnparsed = &address_unparsed
@@ -80,7 +56,7 @@ func (s *OrderService) Find(user *model.Customer, p find.Paging, filt *filters.F
return list, nil return list, nil
} }
func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string, originalUserId uint) error { func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, country_id uint, address_info string) error {
_, err := s.addressesService.ValidateAddressJson(address_info, country_id) _, err := s.addressesService.ValidateAddressJson(address_info, country_id)
if err != nil { if err != nil {
return err return err
@@ -106,10 +82,8 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, co
name = *cart.Name name = *cart.Name
} }
base_price, tax_incl, tax_excl, err := s.getOrderTotalPrice(user_id, cart_id, country_id)
// all checks passed // all checks passed
order, err := s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info, originalUserId, base_price, tax_incl, tax_excl) err = s.ordersRepo.PlaceNewOrder(cart, name, country_id, address_info)
if err != nil { if err != nil {
return err return err
} }
@@ -118,16 +92,20 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, co
// if no error is returned, remove the cart. This should be smooth // if no error is returned, remove the cart. This should be smooth
err = s.cartsRepo.RemoveCart(user_id, cart_id) err = s.cartsRepo.RemoveCart(user_id, cart_id)
if err != nil { if err != nil {
logger.Warn("failed to remove cart after order placement", // Log error but don't fail placing order
"service", "orderService", _ = err
"user_id", user_id,
"cart_id", cart_id,
"error", err.Error(),
)
} }
return s.ChangeOrderStatus(user_id, order.OrderID, enums.OrderStatusPending) // send email to admin
go func(user_id uint) {
err := s.emailService.SendNewOrderPlacedNotification(user_id)
if err != nil {
// Log error but don't fail placing order
_ = err
}
}(user_id)
return nil
} }
func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error { func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, country_id uint, address_info string) error {
@@ -150,57 +128,18 @@ func (s *OrderService) ChangeOrderAddress(user *model.Customer, order_id uint, c
return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info) return s.ordersRepo.ChangeOrderAddress(order_id, country_id, address_info)
} }
func (s *OrderService) ChangeOrderStatus(userId, orderId uint, newStatus enums.OrderStatus) error { // This is obiously just an initial version of this function
order, err := s.ordersRepo.Get(orderId) func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error {
if err != nil { if !user.HasPermission(perms.OrdersModifyAll) {
return err exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
}
if order == nil {
return responseErrors.ErrOrderNotFound
}
if !ValidStatuses[newStatus] {
return responseErrors.ErrInvalidStatus
}
err = s.ordersRepo.ChangeOrderStatus(order.OrderID, newStatus, userId)
if err != nil { if err != nil {
return err return err
} }
actionCtx := orderStatusActions.ActionContext{ if !exists {
Order: order, return responseErrors.ErrUserHasNoSuchOrder
UserId: &userId, }
EmailService: s.emailService,
} }
go func() { return s.ordersRepo.ChangeOrderStatus(order_id, status)
_ = s.actionRegistry.ExecuteForStatus(newStatus, actionCtx)
}()
return nil
}
func (s *OrderService) getOrderTotalPrice(user_id uint, cart_id uint, country_id uint) (float64, float64, float64, error) {
cart, err := s.cartsRepo.RetrieveCart(user_id, cart_id)
if err != nil {
return 0.0, 0.0, 0.0, err
}
base_price := 0.0
tax_incl := 0.0
tax_excl := 0.0
for _, product := range cart.Products {
prices, err := s.productsRepo.GetPrice(product.ProductID, product.ProductAttributeID, constdata.SHOP_ID, user_id, country_id, product.Amount)
if err != nil {
return 0.0, 0.0, 0.0, err
}
base_price += prices.Base
tax_incl += prices.FinalTaxIncl
tax_excl += prices.FinalTaxExcl
}
return base_price, tax_incl, tax_excl, nil
} }

View File

@@ -14,14 +14,6 @@ func GetLangID(c fiber.Ctx) (uint, bool) {
return user_locale.OriginalUser.LangID, true return user_locale.OriginalUser.LangID, true
} }
func GetOriginalUserID(c fiber.Ctx) (uint, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.OriginalUser == nil {
return 0, false
}
return user_locale.OriginalUser.ID, true
}
func GetUserID(c fiber.Ctx) (uint, bool) { func GetUserID(c fiber.Ctx) (uint, bool) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale) user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
if !ok || user_locale.User == nil { if !ok || user_locale.User == nil {

View File

@@ -1,151 +0,0 @@
package logger
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
)
var L *slog.Logger
const (
reset = "\033[0m"
red = "\033[31m"
yellow = "\033[33m"
green = "\033[32m"
blue = "\033[36m"
gray = "\033[90m"
)
type consoleHandler struct {
w io.Writer
colorize bool
}
func (h *consoleHandler) Enabled(ctx context.Context, level slog.Level) bool {
return true
}
func (h *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
level := r.Level.String()
color := reset
switch r.Level {
case slog.LevelError:
color = red
case slog.LevelWarn:
color = yellow
case slog.LevelInfo:
color = blue
case slog.LevelDebug:
color = gray
}
var msg string
if h.colorize {
msg = fmt.Sprintf("%s%s%s %s%s%s %s%s%s",
reset, r.Time.Format("15:04:05"), reset,
color, level, reset,
reset, r.Message, reset)
} else {
msg = fmt.Sprintf("%s %s %s", r.Time.Format("15:04:05"), level, r.Message)
}
var pairs []string
r.Attrs(func(attr slog.Attr) bool {
if h.colorize {
pairs = append(pairs, fmt.Sprintf("%s%s=%s%v%s", green, attr.Key, reset, attr.Value, reset))
} else {
pairs = append(pairs, fmt.Sprintf("%s=%v", attr.Key, attr.Value))
}
return true
})
if len(pairs) > 0 {
msg += " " + strings.Join(pairs, " ")
}
fmt.Fprintln(h.w, msg)
return nil
}
func (h *consoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &consoleHandler{w: h.w, colorize: h.colorize}
}
func (h *consoleHandler) WithGroup(name string) slog.Handler {
return &consoleHandler{w: h.w, colorize: h.colorize}
}
func Init(service string, output io.Writer, level string, colorize bool) {
if output == nil {
output = os.Stderr
}
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
if colorize {
L = slog.New(&consoleHandler{w: output, colorize: true}).With("service", service, "level", lvl.String())
} else {
L = slog.New(slog.NewJSONHandler(output, &slog.HandlerOptions{
Level: lvl,
AddSource: false,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && groups == nil {
a.Key = "timestamp"
}
if a.Key == slog.LevelKey {
a.Key = "level"
}
if a.Key == slog.MessageKey {
a.Key = "message"
}
return a
},
})).With("service", service)
}
}
func Info(msg string, args ...any) {
if L != nil {
L.Info(msg, args...)
}
}
func Warn(msg string, args ...any) {
if L != nil {
L.Warn(msg, args...)
}
}
func Error(msg string, args ...any) {
if L != nil {
L.Error(msg, args...)
}
}
func Debug(msg string, args ...any) {
if L != nil {
L.Debug(msg, args...)
}
}
func With(args ...any) *slog.Logger {
if L == nil {
return nil
}
return L.With(args...)
}

View File

@@ -71,8 +71,6 @@ var (
// Typed errors for orders handler // Typed errors for orders handler
ErrEmptyCart = errors.New("the cart is empty") ErrEmptyCart = errors.New("the cart is empty")
ErrUserHasNoSuchOrder = errors.New("user does not have order with given id") ErrUserHasNoSuchOrder = errors.New("user does not have order with given id")
ErrInvalidStatus = errors.New("invalid order status")
ErrOrderNotFound = errors.New("order not found")
// Typed errors for price reduction handler // Typed errors for price reduction handler
ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'") ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'")
@@ -316,8 +314,7 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrMaxAmtOfAddressesReached), errors.Is(err, ErrMaxAmtOfAddressesReached),
errors.Is(err, ErrUserHasNoSuchAddress), errors.Is(err, ErrUserHasNoSuchAddress),
errors.Is(err, ErrInvalidCountryID), errors.Is(err, ErrInvalidCountryID),
errors.Is(err, ErrInvalidAddressJSON), errors.Is(err, ErrInvalidAddressJSON):
errors.Is(err, ErrInvalidStatus):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrSpecificPriceNotFound): case errors.Is(err, ErrSpecificPriceNotFound):
return fiber.StatusNotFound return fiber.StatusNotFound

12
bo/components.d.ts vendored
View File

@@ -11,7 +11,6 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AddProduct: typeof import('./src/components/admin/AddProduct.vue')['default']
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default'] CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default'] copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
@@ -42,24 +41,15 @@ declare module 'vue' {
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default'] ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default'] ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
Profile: typeof import('./src/components/customer-management/Profile.vue')['default'] Profile: typeof import('./src/components/customer-management/Profile.vue')['default']
RichEditor: typeof import('./src/components/ui/RichEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default'] StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
TabGeneral: typeof import('./src/components/admin/product/TabGeneral.vue')['default']
TabOptions: typeof import('./src/components/admin/product/TabOptions.vue')['default']
TabPricing: typeof import('./src/components/admin/product/TabPricing.vue')['default']
TabQuantities: typeof import('./src/components/admin/product/TabQuantities.vue')['default']
TabSeo: typeof import('./src/components/admin/product/TabSeo.vue')['default']
TabShipping: typeof import('./src/components/admin/product/TabShipping.vue')['default']
TabVariants: typeof import('./src/components/admin/product/TabVariants.vue')['default']
ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default'] ThemeSwitch: typeof import('./src/components/inner/ThemeSwitch.vue')['default']
TopBar: typeof import('./src/components/TopBar.vue')['default'] TopBar: typeof import('./src/components/TopBar.vue')['default']
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default'] TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default'] UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UApp: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default'] UApp: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
UAvatar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Avatar.vue')['default'] UAvatar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Avatar.vue')['default']
UBadge: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default'] UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default'] UCard: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default'] UCheckbox: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
@@ -80,10 +70,10 @@ declare module 'vue' {
UsersList: typeof import('./src/components/admin/UsersList.vue')['default'] UsersList: typeof import('./src/components/admin/UsersList.vue')['default']
UsersSearch: typeof import('./src/components/admin/UsersSearch.vue')['default'] UsersSearch: typeof import('./src/components/admin/UsersSearch.vue')['default']
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default'] USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
USwitch: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default'] UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default'] UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default'] UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
UTooltip: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default']
UTree: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tree.vue')['default'] UTree: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tree.vue')['default']
} }
} }

6889
bo/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,6 @@
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.6.0", "@nuxt/ui": "^4.6.0",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@tiptap/extension-placeholder": "^3.22.3",
"@tiptap/starter-kit": "^3.22.3",
"@tiptap/vue-3": "^3.22.3",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"reka-ui": "^2.9.3", "reka-ui": "^2.9.3",

View File

@@ -1,118 +0,0 @@
<template>
<div class="space-y-6">
<!-- Header bar -->
<div class="flex items-center justify-between flex-wrap gap-3">
<div class="flex items-center gap-3">
<UIcon name="i-lucide-arrow-left"
class="cursor-pointer text-(--text-sky-light) dark:text-(--text-sky-dark) text-xl"
@click="$router.back()" />
<h1 class="text-2xl font-bold text-black dark:text-white">
{{ isEditMode ? 'Edit Product' : 'Add Product' }}
</h1>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500 dark:text-gray-400">Active</span>
<USwitch v-model="store.form.active" color="success" />
</div>
<UButton variant="outline" color="neutral" @click="handleCancel">Cancel</UButton>
<UButton color="info" :loading="store.saving" @click="handleSave">
<UIcon name="i-lucide-save" class="text-base" />
Save
</UButton>
</div>
</div>
<!-- Alerts -->
<UAlert v-if="store.error" color="error" variant="subtle" :title="store.error" icon="i-lucide-alert-circle" />
<UAlert v-if="store.successMessage" color="success" variant="subtle" :title="store.successMessage"
icon="i-lucide-check-circle" />
<!-- Tabs -->
<UTabs :items="tabs" color="info" :ui="{ root: 'gap-6' }">
<template #general>
<ProductTabGeneral />
</template>
<template #pricing>
<ProductTabPricing />
</template>
<template #quantities>
<ProductTabQuantities />
</template>
<template #shipping>
<ProductTabShipping />
</template>
<template #seo>
<ProductTabSeo />
</template>
<template #options>
<ProductTabOptions />
</template>
<template #variants>
<ProductTabVariants :is-edit-mode="isEditMode" />
</template>
</UTabs>
</div>
</template>
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAddProductStore } from '@/stores/admin/addProduct'
import ProductTabGeneral from './product/TabGeneral.vue'
import ProductTabPricing from './product/TabPricing.vue'
import ProductTabQuantities from './product/TabQuantities.vue'
import ProductTabShipping from './product/TabShipping.vue'
import ProductTabSeo from './product/TabSeo.vue'
import ProductTabOptions from './product/TabOptions.vue'
import ProductTabVariants from './product/TabVariants.vue'
const props = defineProps<{
productId?: number
}>()
const router = useRouter()
const store = useAddProductStore()
const isEditMode = computed(() => !!props.productId)
const tabs = computed(() => [
{ label: 'General', slot: 'general' },
{ label: 'Pricing', slot: 'pricing' },
{ label: 'Quantities', slot: 'quantities' },
{ label: 'Shipping', slot: 'shipping' },
{ label: 'SEO', slot: 'seo' },
{ label: 'Options', slot: 'options' },
...(isEditMode.value ? [{ label: 'Variants', slot: 'variants' }] : []),
])
onMounted(async () => {
store.resetForm()
if (isEditMode.value && props.productId) {
await store.loadProduct(props.productId)
await store.loadVariants(props.productId)
}
})
async function handleSave() {
if (!store.form.name.trim()) {
store.error = 'Product name is required.'
return
}
if (isEditMode.value && props.productId) {
await store.updateProduct(props.productId)
} else {
const newId = await store.createProduct()
if (newId) {
router.push({ name: 'admin-product-edit', params: { product_id: newId } })
}
}
}
function handleCancel() {
store.resetForm()
router.push({ name: 'admin-products' })
}
</script>

View File

@@ -1,12 +1,13 @@
<template> <template>
<div class="flex flex-col md:flex-row gap-10"> <div class="flex flex-col md:flex-row gap-10">
<div class="w-full flex flex-col items-center gap-4"> <div class="w-full flex flex-col items-center gap-4">
<UTable :data="usersList" :columns="columns" class="flex-1 w-full" <UTable :data="usersList" :columns="columns" class="flex-1 w-full" :ui="{
:ui="{ root: 'max-w-100wv overflow-auto!' }" /> root: 'max-w-100wv overflow-auto!'
}" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" /> <UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, resolveComponent, h } from 'vue' import { ref, computed, watch, resolveComponent, h } from 'vue'

View File

@@ -2,9 +2,11 @@
<div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)"> <div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)">
<h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search Users</h1> <h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search Users</h1>
<div class="w-full max-w-4xl"> <div class="w-full flex flex-col items-center gap-4">
<UInput icon="i-lucide-search" type="text" placeholder="Type user name or ID..." v-model="searchQuery" <UInput icon="i-lucide-search" type="text" placeholder="Type user name or ID..." v-model="searchQuery"
class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" /> class="flex-1 w-full" :ui="{
root: 'max-w-100wv overflow-auto!'
}" />
</div> </div>
<p v-if="loading">Loading...</p> <p v-if="loading">Loading...</p>
@@ -17,7 +19,7 @@
No users found with that name or ID No users found with that name or ID
</p> </p>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, resolveComponent, h } from 'vue' import { ref, computed, watch, resolveComponent, h } from 'vue'

View File

@@ -1,278 +0,0 @@
<template>
<div class="space-y-5">
{{ addProductStore.form }}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<UFormField label="Product name" name="productName">
<UInput v-model="addProductStore.form.product.name" placeholder="Enter product name" class="w-full" />
</UFormField>
<UFormField label="Reference (SKU)" name="productReference">
<UInput v-model="addProductStore.form.product.reference" placeholder="e.g. REF-001" class="w-full" />
</UFormField>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<UFormField label="Podsumowanie">
<ProductEditor v-model="addProductStore.form.product.description_short" />
</UFormField>
<UFormField label="Description">
<ProductEditor v-model="addProductStore.form.product.description" />
</UFormField>
</div>
<fieldset
class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3 bg-white dark:bg-neutral-900">
<legend class="px-1 block font-medium text-default text-[16px]">Images</legend>
{{ addProductStore.images }}
<div class="flex items-start gap-3 flex-wrap">
<div v-for="(img, idx) in addProductStore.images" :key="idx" class="relative group shrink-0">
<div class="relative w-36 h-36 rounded-xl overflow-hidden border-2 transition-colors" :class="img.cover
? 'border-sky-500'
: 'border-(--border-light) dark:border-(--border-dark)'">
<img :src="img.previewUrl" @error="onImgError"
class="w-full h-full object-contain bg-white dark:bg-neutral-900" />
<div v-if="img.uploading" class="absolute inset-0 flex items-center justify-center bg-black/50">
<UIcon name="svg-spinners:ring-resize" class="text-2xl text-white" />
</div>
<div v-else-if="img.error"
class="absolute bottom-0 left-0 right-0 bg-red-500/80 text-white text-[10px] px-1 py-0.5 text-center truncate">
{{ img.error }}
</div>
<div v-if="img.cover"
class="absolute top-1.5 left-1.5 bg-sky-500 text-white text-[10px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
Cover
</div>
<div v-if="!img.uploading"
class="absolute inset-0 flex flex-col items-center justify-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/50">
<button v-if="!img.cover" type="button"
class="flex items-center gap-1 px-2 py-1 rounded-md bg-white/20 hover:bg-sky-500 text-white text-xs font-medium transition-colors"
@click.stop="addProductStore.setCover(idx)">
<UIcon name="i-lucide-star" class="text-sm" />
Set cover
</button>
<button type="button"
class="flex items-center gap-1 px-2 py-1 rounded-md bg-white/20 hover:bg-red-500 text-white text-xs font-medium transition-colors"
@click.stop="addProductStore.removeImage(idx)">
<UIcon name="i-lucide-trash-2" class="text-sm" />
Remove
</button>
</div>
</div>
</div>
<label
class="w-36 h-36 shrink-0 flex flex-col items-center justify-center gap-1 rounded-xl border-2 border-dashed border-(--border-light) dark:border-(--border-dark) cursor-pointer hover:border-sky-400 hover:text-sky-400 transition-colors text-gray-400"
@dragover.prevent @drop.prevent="onDrop">
<UIcon name="i-lucide-plus" class="text-2xl" />
<span class="text-xs">Add image</span>
<input type="file" accept="image/*" multiple class="hidden" @change="onFileChange" />
</label>
</div>
</fieldset>
<div class="space-y-2">
<p class="text-sm font-medium text-black dark:text-white">Powiązany produkt</p>
<UInput v-model="relatedSearch" placeholder="Search and add Powiązany produkt" icon="i-lucide-search"
class="w-full" />
<div v-if="addProductStore.relatedProducts.length > 0"
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<div v-for="p in addProductStore.relatedProducts" :key="p.id_product"
class="flex items-center justify-between px-3 py-2 text-sm text-black dark:text-white border-b border-(--border-light) dark:border-(--border-dark) last:border-0">
<span>{{ p.name }}</span>
<button type="button" @click="removeRelated(p.id_product)"
class="text-gray-400 hover:text-red-500 transition-colors ml-2">
<UIcon name="i-lucide-x" class="text-base" />
</button>
</div>
</div>
<USelect :items="relatedResults" value-key="product_id" />
<div v-if="relatedResults.length > 0"
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden bg-white dark:bg-neutral-900">
<button v-for="p in relatedResults" :key="p.id_product" type="button"
class="w-full text-left px-3 py-2 text-sm text-black dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors border-b border-(--border-light) dark:border-(--border-dark) last:border-0"
@click="addRelated(p)">
{{ p.name }}
<span class="text-gray-400 ml-1">{{ p.reference }}</span>
</button>
</div>
</div>
<fieldset
class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3 bg-white dark:bg-neutral-900">
<legend class="px-1 block font-medium text-default text-[16px]">Kategorie</legend>
<UInput v-model="categorySearch" placeholder="Search" class="w-52" icon="i-lucide-search" />
<div v-if="addProductStore.selectedCategories.length > 0"
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) p-2 min-h-10 flex flex-wrap gap-2">
<span v-for="cat in addProductStore.selectedCategories" :key="cat.id_category"
class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-sky-100 dark:bg-sky-900 text-sky-700 dark:text-sky-300">
{{ cat.name }}
<button type="button" @click="removeCategory(cat.id_category)"
class="text-sky-500 hover:text-sky-700 dark:hover:text-sky-200 leading-none ml-0.5">
<UIcon name="i-lucide-x" />
</button>
</span>
</div>
<div class="space-y-0.5 overflow-y-auto flex">
<UNavigationMenu orientation="vertical" type="multiple" :items="filteredCategories" :ui="{
root: 'w-auto'
}">
<template #item="{ item }">
<div
class="flex items-center gap-2.5 cursor-pointer rounded px-1.5 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors"
@click.stop="toggleCategory({ id_category: item.params?.category_id, name: item.label as string })">
<UCheckbox :model-value="isCategorySelected(item.params?.category_id)"
color="info" />
<span class="text-sm text-black dark:text-white">{{ item.label }}</span>
</div>
</template>
</UNavigationMenu>
</div>
<UButton size="xs" variant="outline" color="neutral" icon="i-lucide-plus">
Add new category
</UButton>
</fieldset>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useAddProductStore } from '@/stores/admin/addProduct'
import type { ProductCategory, ProductRelated } from '@/types/product'
import errorImg from '@/assets/error.svg'
import { useFetchJson } from '@/composable/useFetchJson'
import { watch } from 'vue'
import type { NavigationMenuItem } from '@nuxt/ui'
const addProductStore = useAddProductStore()
const loading = ref(false)
function onImgError(e: Event) {
const target = e.target as HTMLImageElement
target.src = errorImg
}
function onFileChange(e: Event) {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) addProductStore.addImageFiles(files)
; (e.target as HTMLInputElement).value = ''
}
function onDrop(e: DragEvent) {
const files = e.dataTransfer?.files
if (files && files.length > 0) addProductStore.addImageFiles(files)
}
// categories
const categorySearch = ref('')
const allCategories = computed(() => adaptCategory(addProductStore.categories))
function adaptCategory(menu: NavigationMenuItem[]) {
for (const item of menu) {
if (item.children && item.children.length > 0) {
item.open = true
adaptCategory(item.children);
item.children.unshift({
label: item.label,
icon: 'i-lucide-book-open',
params: item.params,
})
} else {
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
//there have to be request on filter category (but have to return like parent => children =>)
const filteredCategories = computed(() => {
const q = categorySearch.value.trim().toLowerCase()
if (!q) return allCategories.value
return allCategories.value.filter(c => c.name.toLowerCase().includes(q))
})
function isCategorySelected(id: number) {
return addProductStore.selectedCategories.some(c => c.id_category === id)
}
function toggleCategory(cat: ProductCategory) {
console.log(cat);
if (isCategorySelected(cat.id_category)) {
removeCategory(cat.id_category)
} else {
addProductStore.selectedCategories.push(cat)
}
}
function removeCategory(id: number) {
const idx = addProductStore.selectedCategories.findIndex(c => c.id_category === id)
if (idx !== -1) addProductStore.selectedCategories.splice(idx, 1)
}
// related products
const relatedSearch = ref('')
const relatedResults = ref<ProductRelated[]>([])
let searchTimer: ReturnType<typeof setTimeout> | null = null
watch(relatedSearch, (q) => {
if (searchTimer) clearTimeout(searchTimer)
if (!q.trim()) { relatedResults.value = []; return }
searchTimer = setTimeout(async () => {
await fetchProducts(q)
}, 1000)
})
function addRelated(product: ProductRelated) {
if (!addProductStore.relatedProducts.some(p => p.id_product === product.id_product)) {
addProductStore.relatedProducts.push(product)
}
relatedSearch.value = ''
relatedResults.value = []
}
function removeRelated(id: number) {
const idx = addProductStore.relatedProducts.findIndex(p => p.id_product === id)
if (idx !== -1) addProductStore.relatedProducts.splice(idx, 1)
}
const products = ref()
async function fetchProducts(q: string) {
if (!q.trim()) {
products.value = []
return
}
loading.value = true
relatedResults.value = []
try {
const query = `name=~${q.trim()}`
const result = await useFetchJson(
`/api/v1/restricted/product/list?${query}`
)
relatedResults.value = (result.items ?? result) as ProductRelated[]
} catch (e) {
console.log(e);
} finally {
loading.value = false
}
}
await addProductStore.loadCategories()
</script>

View File

@@ -1,47 +0,0 @@
<template>
<div class="card-section space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<UFormField label="Visibility">
<USelect v-model="store.form.visibility" :options="visibilityOptions" value-key="value"
label-key="label" class="w-full" />
</UFormField>
<UFormField label="Condition">
<USelect v-model="store.form.condition" :options="conditionOptions" value-key="value"
label-key="label" class="w-full" />
</UFormField>
<UFormField label="EAN-13 / JAN barcode">
<UInput v-model="store.form.ean13" placeholder="4006381333931" :maxlength="13" class="w-full" />
</UFormField>
<UFormField label="UPC barcode">
<UInput v-model="store.form.upc" placeholder="012345678905" :maxlength="12" class="w-full" />
</UFormField>
<UFormField label="ISBN">
<UInput v-model="store.form.isbn" placeholder="978-3-16-148410-0" :maxlength="32"
class="w-full" />
</UFormField>
</div>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
const store = useAddProductStore()
const visibilityOptions = [
{ value: 'both', label: 'Everywhere (catalog & search)' },
{ value: 'catalog', label: 'Catalog only' },
{ value: 'search', label: 'Search only' },
{ value: 'none', label: 'Hidden' },
]
const conditionOptions = [
{ value: 'new', label: 'New' },
{ value: 'used', label: 'Used' },
{ value: 'refurbished', label: 'Refurbished' },
]
</script>

View File

@@ -1,26 +0,0 @@
<template>
<div class="card-section space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<UFormField label="Price (net)">
<UInputNumber v-model="store.form.price" :min="0" :step="0.01"
:format-options="{ minimumFractionDigits: 2 }" class="w-full" />
</UFormField>
<UFormField label="Wholesale price (net)">
<UInputNumber v-model="store.form.wholesale_price" :min="0" :step="0.01"
:format-options="{ minimumFractionDigits: 2 }" class="w-full" />
</UFormField>
</div>
<div class="flex items-center gap-3">
<USwitch v-model="store.form.on_sale" color="warning" />
<span class="text-sm text-black dark:text-white">Show "On Sale" badge on this product</span>
</div>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
const store = useAddProductStore()
</script>

View File

@@ -1,30 +0,0 @@
<template>
<div class="card-section space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<UFormField label="Stock quantity">
<UInputNumber v-model="store.form.quantity" :min="0" class="w-full" />
</UFormField>
<UFormField label="Minimum order quantity">
<UInputNumber v-model="store.form.minimal_quantity" :min="1" class="w-full" />
</UFormField>
</div>
<UFormField label="When out of stock">
<USelect v-model="store.form.out_of_stock" :options="outOfStockOptions" value-key="value"
label-key="label" class="w-72" />
</UFormField>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
const store = useAddProductStore()
const outOfStockOptions = [
{ value: 0, label: 'Deny orders' },
{ value: 1, label: 'Allow orders' },
{ value: 2, label: 'Use shop default' },
]
</script>

View File

@@ -1,31 +0,0 @@
<template>
<div class="card-section space-y-6">
<p class="text-sm text-gray-400">
Leave blank to automatically use the product name. Helps search engines find your product.
</p>
<UFormField label="Meta title" hint="Recommended: max 70 characters">
<UInput v-model="store.form.meta_title" placeholder="Leave blank to use product name"
:maxlength="128" class="w-full" />
</UFormField>
<UFormField label="Meta description" hint="Recommended: max 160 characters">
<UTextarea v-model="store.form.meta_description"
placeholder="Brief description shown in search results" :rows="3" :maxlength="512"
class="w-full" />
</UFormField>
<UFormField label="URL slug">
<UInput v-model="store.form.link_rewrite" placeholder="auto-generated-from-name" class="w-full" />
<template #hint>
<span class="text-xs text-gray-400">Lowercase letters, numbers and hyphens only.</span>
</template>
</UFormField>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
const store = useAddProductStore()
</script>

View File

@@ -1,32 +0,0 @@
<template>
<div class="card-section space-y-6">
<p class="text-sm text-gray-400">
Package dimensions are used to calculate shipping costs.
</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-5">
<UFormField label="Width (cm)">
<UInputNumber v-model="store.form.width" :min="0" :step="0.01" class="w-full" />
</UFormField>
<UFormField label="Height (cm)">
<UInputNumber v-model="store.form.height" :min="0" :step="0.01" class="w-full" />
</UFormField>
<UFormField label="Depth (cm)">
<UInputNumber v-model="store.form.depth" :min="0" :step="0.01" class="w-full" />
</UFormField>
<UFormField label="Weight (kg)">
<UInputNumber v-model="store.form.weight" :min="0" :step="0.001" class="w-full" />
</UFormField>
</div>
<UFormField label="Delivery days" hint="Days to ship from local warehouse to customer">
<UInputNumber v-model="store.form.delivery_days" :min="0" class="w-40" />
</UFormField>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
const store = useAddProductStore()
</script>

View File

@@ -1,120 +0,0 @@
<template>
<div class="card-section space-y-4">
<div v-if="store.loading" class="flex justify-center py-10">
<UIcon name="svg-spinners:ring-resize" class="text-3xl text-primary" />
</div>
<div v-else-if="store.variants.length === 0" class="flex flex-col items-center gap-3 py-10 text-gray-400">
<UIcon name="i-lucide-layers" class="text-4xl" />
<p class="text-sm text-center">
{{ isEditMode ? 'No variants found for this product.' : 'Save the product first, then variants will appear here.' }}
</p>
</div>
<template v-else>
<p class="text-sm text-gray-400">
Each variant is a combination of attributes (e.g. Color: Red / Size: M).
Edit price offset, stock and other details per variant independently.
</p>
<div v-for="variant in store.variants" :key="variant.id_product_attribute"
class="border border-(--border-light) dark:border-(--border-dark) rounded-xl p-4 space-y-4">
<!-- Variant header -->
<div class="flex items-center justify-between flex-wrap gap-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-black dark:text-white text-sm">
{{ variant.attribute_label || `Variant #${variant.id_product_attribute}` }}
</span>
<UBadge v-if="variant.default_on" color="success" variant="subtle" size="sm">
Default
</UBadge>
</div>
<UButton size="sm" color="info"
:loading="!!store.variantSaving[variant.id_product_attribute!]"
@click="handleSaveVariant(variant)">
Save variant
</UButton>
</div>
<UAlert v-if="store.variantErrors[variant.id_product_attribute!]" color="error"
variant="subtle" :title="store.variantErrors[variant.id_product_attribute!]" />
<!-- Variant fields -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<UFormField label="Reference">
<UInput v-model="variant.reference" placeholder="REF-001-A" class="w-full" />
</UFormField>
<UFormField label="EAN-13">
<UInput v-model="variant.ean13" placeholder="4006381333931" :maxlength="13"
class="w-full" />
</UFormField>
<UFormField label="Price offset">
<UInputNumber v-model="variant.price" :step="0.01"
:format-options="{ minimumFractionDigits: 2 }" class="w-full" />
</UFormField>
<UFormField label="Wholesale price">
<UInputNumber v-model="variant.wholesale_price" :min="0" :step="0.01"
:format-options="{ minimumFractionDigits: 2 }" class="w-full" />
</UFormField>
<UFormField label="Stock quantity">
<UInputNumber v-model="variant.quantity" :min="0" class="w-full" />
</UFormField>
<UFormField label="Min. quantity">
<UInputNumber v-model="variant.minimal_quantity" :min="1" class="w-full" />
</UFormField>
<UFormField label="Weight offset (kg)">
<UInputNumber v-model="variant.weight" :step="0.001" class="w-full" />
</UFormField>
<UFormField label="">
<div class="flex items-center gap-2 pt-6">
<USwitch v-model="variant.default_on" color="success"
@update:model-value="onSetDefault(variant)" />
<span class="text-sm text-black dark:text-white">Set as default</span>
</div>
</UFormField>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { useAddProductStore } from '@/stores/admin/addProduct'
import type { ProductVariantForm } from '@/types/product'
const props = defineProps<{ isEditMode: boolean }>()
const store = useAddProductStore()
async function handleSaveVariant(variant: ProductVariantForm) {
if (!variant.id_product_attribute) return
await store.saveVariant(variant.id_product_attribute, {
reference: variant.reference,
ean13: variant.ean13,
price: variant.price,
wholesale_price: variant.wholesale_price,
quantity: variant.quantity,
minimal_quantity: variant.minimal_quantity,
weight: variant.weight,
default_on: variant.default_on,
})
}
function onSetDefault(selected: ProductVariantForm) {
if (!selected.default_on) return
store.variants.forEach(v => {
if (v.id_product_attribute !== selected.id_product_attribute) v.default_on = false
})
}
</script>

View File

@@ -1,89 +1,77 @@
<template> <template>
<div class="flex flex-col gap-5"> <div class="">
<div class="flex justify-between items-center"> <div class="flex flex-col gap-5 mb-6">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Addresses') }}</h1>
<UButton color="info" @click="openModal()"> <div class="flex md:flex-row flex-col justify-between items-start md:items-center gap-5 md:gap-0">
<div class="flex gap-2 items-center">
<UInput v-model="searchQuery" type="text" :placeholder="t('Search address')"
class="bg-white dark:bg-gray-800 text-black dark:text-white absolute" />
<UIcon name="ic:baseline-search"
class="text-[20px] text-(--text-sky-light) dark:text-(--text-sky-dark) relative left-40" />
</div>
<UButton color="info" @click="openCreateModal">
<UIcon name="mdi:add-bold" /> <UIcon name="mdi:add-bold" />
{{ t('Add Address') }} {{ t('Add Address') }}
</UButton> </UButton>
</div> </div>
<div v-if="store.loading" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ t('Loading...') }}
</div> </div>
<div v-else-if="store.error" class="text-center py-8 text-red-500"> <div v-if="cartStore.addressLoading" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ store.error }} {{ t('Loading addresses...') }}
</div> </div>
<div v-else-if="store.addresses.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div v-else-if="cartStore.addressError" class="text-center py-8 text-red-500 dark:text-red-400">
<div v-for="addr in store.addresses" :key="addr.id" {{ cartStore.addressError }}
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) flex justify-between">
<div class="flex flex-col gap-1">
<p class="font-semibold text-black dark:text-white">{{ addr.address_unparsed.recipient }}</p>
<p class="text-sm text-black dark:text-white">
{{ addr.address_unparsed.street }} {{ addr.address_unparsed.building_no
}}{{ addr.address_unparsed.apartment_no ? '/' + addr.address_unparsed.apartment_no : '' }}
</p>
<p class="text-sm text-black dark:text-white">
{{ addr.address_unparsed.postal_code }}, {{ addr.address_unparsed.city }}
</p>
</div> </div>
<div class="flex flex-col items-end justify-between"> <div v-else-if="cartStore.paginatedAddresses.length"
<UButton size="xs" color="error" variant="ghost" :title="t('Delete')" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
@click="confirmDelete(addr.id)"> <div v-for="address in cartStore.paginatedAddresses" :key="address.id"
class="border border-(--border-light) dark:border-(--border-dark) rounded-md p-4 bg-(--second-light) dark:bg-(--main-dark) hover:shadow-md transition-shadow flex justify-between">
<div class="flex flex-col gap-2 items-start justify-end">
<p class="text-black dark:text-white font-semibold">{{ address.address_info.recipient }}</p>
<p class="text-black dark:text-white">{{ address.address_info.street }} {{
address.address_info.building_no }}{{ address.address_info.apartment_no ? '/' +
address.address_info.apartment_no : '' }}</p>
<p class="text-black dark:text-white">{{ address.address_info.postal_code }}, {{
address.address_info.city }}</p>
<p class="text-black dark:text-white">{{ address.address_info.voivodeship }}</p>
<p v-if="address.address_info.address_line2" class="text-black dark:text-white">{{
address.address_info.address_line2 }}</p>
</div>
<div class="flex flex-col items-end justify-between gap-2">
<button @click="confirmDelete(address.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[18px]" /> <UIcon name="material-symbols:delete" class="text-[18px]" />
</UButton> </button>
<UButton size="sm" color="neutral" variant="outline" @click="openModal(addr)"> <UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)"
class="text-(--text-sky-light) dark:text-(--text-sky-dark) text-[13px]">
{{ t('edit') }} {{ t('edit') }}
<UIcon name="ic:sharp-edit" class="text-[14px]" /> <UIcon name="ic:sharp-edit" class="text-[15px]" />
</UButton> </UButton>
</div> </div>
</div> </div>
</div> </div>
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400"> <div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
{{ t('No addresses found') }} <div class="mt-6 flex justify-center">
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
</div> </div>
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
<UModal v-model:open="showModal"> <template #content>
<template #header> <div class="p-6 flex flex-col gap-6">
<h3 class="text-lg font-semibold text-black dark:text-white"> <p class="text-[20px] text-black dark:text-white ">Address</p>
{{ editingId ? t('Edit Address') : t('Add Address') }} <UForm @submit="saveAddress" class="space-y-4" :validate="validate" :state="formData">
</h3> <template v-for="field in formFieldKeys" :key="field">
</template> <UFormField :label="fieldLabel(field)" :name="field"
<template #body> :required="field !== 'address_line2'">
<div class="flex flex-col gap-5">
<USelectMenu v-model="selectedCountry" :items="countries" class="w-full"
@update:model-value="onCountryChange" :searchInput="false">
<template #default>
<div class="flex flex-col items-start leading-tight">
<span class="text-xs text-gray-400">{{ t('Country') }}</span>
<span v-if="selectedCountry" class="font-medium text-black dark:text-white">
{{ selectedCountry.name }}
</span>
<span v-else class="text-gray-400">{{ t('Select country') }}</span>
</div>
</template>
<template #item-leading="{ item }">
<span class="text-lg mr-1">{{ item.flag }} {{ item.name }}</span>
</template>
</USelectMenu>
<div v-if="templateLoading" class="text-center py-4 text-gray-500 dark:text-gray-400">
{{ t('Loading...') }}
</div>
<p v-else-if="!selectedCountry" class="text-center text-sm text-gray-400 dark:text-gray-500">
{{ t('Select a country to continue') }}
</p>
<UForm v-else :validate="validate" :state="formData" @submit="save" class="space-y-4">
<UFormField v-for="field in templateKeys" :key="field" :label="fieldLabel(field)" :name="field"
:required="!optionalFields.has(field)">
<UInput v-model="formData[field]" :placeholder="fieldLabel(field)" class="w-full" /> <UInput v-model="formData[field]" :placeholder="fieldLabel(field)" class="w-full" />
</UFormField> </UFormField>
</template>
<div class="flex justify-end gap-2 pt-2"> <div class="flex justify-end gap-2">
<UButton variant="outline" color="neutral" @click="showModal = false"> <UButton variant="outline" color="neutral" @click="closeModal">
{{ t('Cancel') }} {{ t('Cancel') }}
</UButton> </UButton>
<UButton type="submit" color="info">
<UButton type="submit" color="info" class="cursor-pointer">
{{ t('Save') }} {{ t('Save') }}
</UButton> </UButton>
</div> </div>
@@ -91,59 +79,78 @@
</div> </div>
</template> </template>
</UModal> </UModal>
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
<UModal v-model:open="showDeleteConfirm"> <template #content>
<template #body> <div class="p-6 flex flex-col gap-3">
<div class="flex flex-col items-center gap-3 py-2"> <div class="flex flex-col gap-2 justify-center items-center">
<UIcon name="f7:exclamationmark-triangle" class="text-[40px] text-red-600" /> <p class="flex items-end gap-2 dark:text-white text-black">
<p class="font-semibold text-black dark:text-white">{{ t('Confirm Delete') }}</p> <UIcon name='f7:exclamationmark-triangle' class="text-[35px] text-red-700" />
<p class="text-sm text-gray-600 dark:text-gray-300"> Confirm Delete
{{ t('Are you sure you want to delete this address?') }}
</p> </p>
<p class="text-gray-700 dark:text-gray-300">
{{ t('Are you sure you want to delete this address?') }}</p>
</div> </div>
</template> <div class="flex justify-center gap-5">
<template #footer> <UButton variant="outline" color="neutral" @click="showDeleteConfirm = false"
<div class="flex justify-center gap-4"> class="dark:text-white text-black">{{ t('Cancel') }}
<UButton variant="outline" color="neutral" @click="showDeleteConfirm = false">
{{ t('Cancel') }}
</UButton>
<UButton variant="outline" color="error" @click="deleteAddress">
{{ t('Delete') }}
</UButton> </UButton>
<UButton variant="outline" color="neutral" @click="deleteAddress" class="text-red-700">
{{ t('Delete') }}</UButton>
</div>
</div> </div>
</template> </template>
</UModal> </UModal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed } from 'vue' import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useCartStore } from '@/stores/customer/cart'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { countries } from '@/router/langs' import { currentCountry } from '@/router/langs'
import { useAddressStore } from '@/stores/customer/address'
import type { Country } from '@/types'
import type { Address } from '@/stores/customer/address'
type AddressFormState = Record<string, string>
const cartStore = useCartStore()
const { t } = useI18n() const { t } = useI18n()
const store = useAddressStore() const searchQuery = ref(cartStore.addressSearchQuery)
// --- Modal state ---
const showModal = ref(false) const showModal = ref(false)
const editingId = ref<number | null>(null) const isEditing = ref(false)
const selectedCountry = ref<Country | null>(null) const editingAddressId = ref<number | null>(null)
const templateLoading = ref(false) const addressTemplate = ref<AddressFormState | null>(null)
const template = ref<Record<string, string>>({}) const formData = reactive<AddressFormState>({})
const formData = reactive<Record<string, string>>({})
const templateKeys = computed(() => Object.keys(template.value))
const optionalFields = new Set(['address_line2'])
// --- Delete state ---
const showDeleteConfirm = ref(false) const showDeleteConfirm = ref(false)
const deleteId = ref<number | null>(null) const addressToDelete = ref<number | null>(null)
const fieldLabels: Record<string, string> = { const page = computed<number>({
recipient: 'Recipient', get: () => cartStore.addressCurrentPage,
set: (value: number) => cartStore.setAddressPage(value)
})
const totalItems = computed(() => cartStore.totalAddressItems)
const pageSize = cartStore.addressPageSize
const formFieldKeys = computed(() => (addressTemplate.value ? Object.keys(addressTemplate.value) : []))
watch(searchQuery, (val) => {
cartStore.setAddressSearchQuery(val)
})
onMounted(() => {
cartStore.fetchAddresses()
})
function clearFormData() {
Object.keys(formData).forEach((key) => delete formData[key])
}
function fieldLabel(key: string) {
const labels: Record<string, string> = {
postal_code: 'Zip Code',
post_town: 'City',
city: 'City',
county: 'County',
region: 'Region',
voivodeship: 'Region / Voivodeship',
street: 'Street', street: 'Street',
thoroughfare: 'Street', thoroughfare: 'Street',
building_no: 'Building No', building_no: 'Building No',
@@ -152,88 +159,90 @@ const fieldLabels: Record<string, string> = {
orientation_number: 'Orientation Number', orientation_number: 'Orientation Number',
apartment_no: 'Apartment No', apartment_no: 'Apartment No',
sub_building: 'Sub Building', sub_building: 'Sub Building',
postal_code: 'Zip Code', address_line2: 'Address Line 2',
post_town: 'City', recipient: 'Recipient'
city: 'City', }
county: 'County',
region: 'Region', return t(labels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (chr) => chr.toUpperCase()))
voivodeship: 'Region / Voivodeship',
address_line2: 'Address Line 2'
} }
function fieldLabel(key: string) { async function openCreateModal() {
return t(fieldLabels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())) resetForm()
} isEditing.value = false
function validate() { const template = await cartStore.getAddressTemplate(Number(currentCountry.value?.id) | 2).catch(() => null)
return templateKeys.value if (template) {
.filter((key) => !optionalFields.has(key) && !formData[key]?.trim()) addressTemplate.value = template
.map((key) => ({ name: key, message: t(`${fieldLabel(key)} is required`) })) clearFormData()
} Object.assign(formData, template)
function applyTemplate(tpl: Record<string, string>, existing?: Record<string, string>) {
Object.keys(formData).forEach((k) => delete formData[k])
Object.keys(tpl).forEach((k) => {
formData[k] = existing?.[k] ?? ''
})
}
function openModal(addr?: Address) {
template.value = {}
Object.keys(formData).forEach((k) => delete formData[k])
if (addr) {
editingId.value = addr.id
selectedCountry.value = countries.find((c) => c.id === addr.country_id) ?? null
loadTemplate(addr.country_id, addr.address_unparsed)
} else {
editingId.value = null
selectedCountry.value = null
} }
showModal.value = true showModal.value = true
} }
async function onCountryChange(country: Country | null) { async function openEditModal(address: any) {
if (!country) { currentCountry.value = address.country_id || 1
template.value = {} const template = await cartStore.getAddressTemplate(address.country_id | 2).catch(() => null)
Object.keys(formData).forEach((k) => delete formData[k])
return if (template) {
addressTemplate.value = template
clearFormData()
Object.assign(formData, template)
} }
await loadTemplate(country.id)
if (address.address_info) {
formFieldKeys.value.forEach((key) => {
formData[key] = address.address_info[key] ?? ''
})
}
isEditing.value = true
editingAddressId.value = address.id
showModal.value = true
} }
async function loadTemplate(countryId: number, existing?: Record<string, string>) { function resetForm() {
templateLoading.value = true clearFormData()
try { editingAddressId.value = null
const tpl = await store.getTemplate(countryId)
template.value = tpl
applyTemplate(tpl, existing)
} finally {
templateLoading.value = false
}
} }
async function save() { function closeModal() {
if (!selectedCountry.value) return
if (editingId.value) {
await store.updateAddress(editingId.value, selectedCountry.value.id, { ...formData })
} else {
await store.createAddress(selectedCountry.value.id, { ...formData })
}
showModal.value = false showModal.value = false
resetForm()
}
function validate() {
const errors: Array<{ name: string; message: string }> = []
const optionalFields = new Set(['address_line2'])
formFieldKeys.value.forEach((key) => {
if (!optionalFields.has(key) && !formData[key]?.trim()) {
errors.push({ name: key, message: `${fieldLabel(key)} required` })
}
})
return errors
}
async function saveAddress() {
if (isEditing.value && editingAddressId.value) {
await cartStore.updateAddress(editingAddressId.value, currentCountry.value?.id || 2, formData)
} else {
await cartStore.addAddress(currentCountry.value?.id || 2, formData)
}
closeModal()
} }
function confirmDelete(id: number) { function confirmDelete(id: number) {
deleteId.value = id addressToDelete.value = id
showDeleteConfirm.value = true showDeleteConfirm.value = true
} }
async function deleteAddress() { async function deleteAddress() {
if (deleteId.value) await store.deleteAddress(deleteId.value) if (addressToDelete.value) {
await cartStore.deleteAddress(addressToDelete.value)
}
showDeleteConfirm.value = false showDeleteConfirm.value = false
deleteId.value = null addressToDelete.value = null
} }
store.fetchAddresses()
</script> </script>

View File

@@ -1,10 +1,15 @@
<template> <template>
<div class="flex flex-col gap-5 md:gap-10"> <div class="flex flex-col gap-5 md:gap-10">
<div class="flex flex-col md:flex-row justify-between items-center gap-4"> <div class="flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-start gap-2">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Carts') }}</h1> <h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Carts') }}</h1>
<div class="flex gap-3"> <UTooltip :delay-duration="0" text="You can create up to 10 carts.">
<UButton color="primary" @click="showCreateModal = true" :disabled="cartStore.carts?.length >= 10" <UIcon name="icon-park-outline:attention" class="text-[15px] text-blue-500" />
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)"> </UTooltip>
</div>
<div class="flex items-start gap-1">
<UButton color="info" @click="showCreateModal = true" :disabled="cartStore.carts?.length >= 10"
class="">
<UIcon name="mdi:plus" class="mr-1" /> <UIcon name="mdi:plus" class="mr-1" />
{{ t('New Cart') }} {{ t('New Cart') }}
</UButton> </UButton>
@@ -44,25 +49,23 @@
</div> </div>
<UModal v-model:open="showCreateModal"> <UModal v-model:open="showCreateModal">
<template #header>
<h3 class="text-lg font-semibold text-black dark:text-white">{{ t('Create New Cart') }}</h3>
</template>
<template #body> <template #body>
<div class="flex flex-col gap-5">
<h3 class="text-xl font-bold text-black dark:text-white text-center">{{ t('Create New Cart') }}</h3>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<UInput v-model="newCartName" :placeholder="t('Cart name')" <UInput v-model="newCartName" :placeholder="t('Cart name')"
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" /> class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
</div> </div>
</template>
<template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<UButton variant="outline" color="neutral" @click="showCreateModal = false"> <UButton variant="outline" color="neutral" @click="showCreateModal = false">
{{ t('Cancel') }} {{ t('Cancel') }}
</UButton> </UButton>
<UButton color="primary" @click="createCart" :disabled="!newCartName.trim()" <UButton color="info" @click="createCart" :disabled="!newCartName.trim()"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white"> class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
{{ t('Create') }} {{ t('Create') }}
</UButton> </UButton>
</div> </div>
</div>
</template> </template>
</UModal> </UModal>
</template> </template>

View File

@@ -1,19 +1,11 @@
<template> <template>
<suspense> <suspense>
<div class=""> <div class="">
<!-- <UNavigationMenu orientation="vertical" :items="listing" class="data-[orientation=vertical]:w-48">
<template #item="{ item, active }">
<div class="flex items-center gap-2 px-3 py-2">
<UIcon name="i-heroicons-book-open" />
<span>{{ item.name }}</span>
</div>
</template>
</UNavigationMenu> -->
<h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1> <h1 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">Products</h1>
<div v-if="customerProductStore.loading" class="text-center py-8"> <!-- <div v-if="customerProductStore.loading" class="text-center py-8">
<span class="text-gray-600 dark:text-gray-400">Loading products...</span> <span class="text-gray-600 dark:text-gray-400">Loading products...</span>
</div> </div> -->
<div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded"> <div v-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ customerProductStore.error }} {{ customerProductStore.error }}
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
@@ -21,8 +13,7 @@
<CategoryMenu /> <CategoryMenu />
<UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1"> <UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }"> <template #expanded="{ row }">
<UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild" <UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild" :ui="{
:ui="{
thead: 'hidden' thead: 'hidden'
}" /> }" />
</template> </template>
@@ -42,95 +33,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue' import { ref, watch, h, resolveComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/CategoryMenu.vue' import CategoryMenu from '../inner/CategoryMenu.vue'
import { useCustomerProductStore } from '@/stores/customer/customer-product' import { useCustomerProductStore } from '@/stores/customer/customer-product'
import type { Product } from '@/stores/customer/customer-product' import type { Product } from '@/stores/customer/customer-product'
import { useCartStore } from '@/stores/customer/cart' import { useCartStore } from '@/stores/customer/cart'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js' import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useTableState } from '@/composable/useTableState'
import { debounce } from '@/composable/useDebouncedSearch'
const customerProductStore = useCustomerProductStore() const customerProductStore = useCustomerProductStore()
const router = useRouter() const { page, sortField, filters } = useTableState()
const route = useRoute()
const page = computed({
get: () => Number(route.query.page) || 1,
set: (val: number) => {
router.push({
query: {
...route.query,
page: val
}
})
}
})
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const currentSort = route.query.sort as string | undefined
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
let query = { ...route.query } function getIcon(name: string) {
if (sortField.value[0] === name) {
if (currentSort === sort) { if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (currentDirection === 'asc') { if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
query.direction = 'desc'
} else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
query.direction = 'asc'
query.sort = sort
}
} else {
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(key => {
if (!['page', 'sort', 'direction'].includes(key)) {
delete baseQuery[key]
}
})
router.push({
query: {
...baseQuery,
...val,
page: 1
}
})
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
} }
return 'i-lucide-arrow-up-down'
} }
const updateFilter = debounce((columnId: string, val: string) => { const updateFilter = debounce((columnId: string, val: string) => {
@@ -142,29 +66,11 @@ const updateFilter = debounce((columnId: string, val: string) => {
filters.value = newFilters filters.value = newFilters
}, 400) }, 400)
function goToProduct(productId: number) {
router.push({
name: 'customer-product-details',
params: { product_id: productId }
})
}
const selectedCount = ref({ const selectedCount = ref({
product_id: null, product_id: null as number | null,
count: 0 count: 0
}) })
function getIcon(name: string) {
if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const UInputNumber = resolveComponent('UInputNumber') const UInputNumber = resolveComponent('UInputNumber')
const UInput = resolveComponent('UInput') const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
@@ -215,7 +121,6 @@ const columns: TableColumn<Product>[] = [
}) })
]) ])
}, },
// header: '#',
cell: ({ row }) => `#${row.getValue('product_id') as number}` cell: ({ row }) => `#${row.getValue('product_id') as number}`
}, },
{ {
@@ -442,6 +347,15 @@ const columnsChild: TableColumn<Product>[] = [
} }
] ]
const router = useRouter()
function goToProduct(productId: number) {
router.push({
name: 'customer-product-details',
params: { product_id: productId }
})
}
const cartStore = useCartStore() const cartStore = useCartStore()
const toast = useToast() const toast = useToast()
async function addToCart(product_id: number) { async function addToCart(product_id: number) {
@@ -465,11 +379,17 @@ async function addToCart(product_id: number) {
duration: 5000 duration: 5000
}) })
} }
watch( watch(
() => route.query, [filters, sortField, page],
() => { () => {
customerProductStore.fetchProductList() customerProductStore.fetchProductList({
...filters.value,
sort: sortField.value[0],
direction: sortField.value[1],
page: page.value
})
}, },
{ immediate: true } { immediate: true, deep: true }
) )
</script> </script>

View File

@@ -1,10 +1,14 @@
<template> <template>
<div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)"> <div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)">
<h1 class="text-6xl font-bold text-black dark:text-white mb-14"> <h1 class="text-6xl font-bold text-black dark:text-white mb-14">
Search Products Search Products
</h1> </h1>
<div class="w-full max-w-4xl"> <div class="w-full max-w-4xl flex flex-col gap-2">
<div class="flex items-center gap-2 mt-3 text-gray-600 dark:text-gray-300">
<UCheckbox v-model="searchByReference" />
<span>Search by reference (ID)</span>
</div>
<UInput icon="i-lucide-search" type="text" placeholder="Type product name or ID..." <UInput icon="i-lucide-search" type="text" placeholder="Type product name or ID..."
v-model="searchQuery" class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" /> v-model="searchQuery" class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
</div> </div>
@@ -13,142 +17,348 @@
Loading... Loading...
</div> </div>
<div v-else-if="products.length" class="mt-6 w-full"> <p v-else-if="error" class="mt-6 text-red-500">
<UTable :data="products" :columns="columns" class="flex-1 w-full" {{ error }}
:ui="{ root: 'max-w-100wv overflow-auto!' }" /> </p>
<div v-else-if="products.length" class="flex items-center w-full">
<UTable :data="products" :columns="columns" class="mt-7" :ui="{ root: 'w-full!' }" />
</div> </div>
<p v-else-if="searchQuery" class="mt-6"> <p v-else-if="searchQuery" class="mt-6">
No products found No products found
</p> </p>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useFetchJson } from '@/composable/useFetchJson' import { ref, watch } from 'vue'
import { ref, watch, computed, resolveComponent } from 'vue' import { h, resolveComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { debounce } from 'chart.js/helpers'
import { h } from 'vue'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import type { Product } from '@/types/product' import type { Product } from '@/types/product'
import errorImg from '@/assets/error.svg' import { useProductSearchApi } from '@/composable/useProductSearchApi'
import { useTableState } from '@/composable/useTableState'
import { debounce } from '@/composable/useDebouncedSearch'
const searchQuery = ref('') const {
const products = ref<Product[]>([]) searchQuery,
const loading = ref(false) products,
const error = ref<string | null>(null) loading,
error,
searchByReference,
selectedCount,
fetchProducts,
addToCart,
goToProduct
} = useProductSearchApi()
const { sortField, filters } = useTableState()
async function fetchProducts() {
if (!searchQuery.value.trim()) {
products.value = []
return
}
loading.value = true
error.value = null
try {
const query = `name=~${searchQuery.value.trim()}`
const result = await useFetchJson(
`/api/v1/restricted/product/list?${query}`
)
products.value = result.items || result
} catch (e) {
error.value = 'Failed to load products'
} finally {
loading.value = false
}
}
const debouncedFetch = debounce(fetchProducts, 400) const debouncedFetch = debounce(fetchProducts, 400)
watch(searchQuery, () => { watch([searchQuery, searchByReference], () => {
debouncedFetch() debouncedFetch()
}) })
const route = useRoute() const UInputNumber = resolveComponent('UInputNumber')
const router = useRouter()
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const query = { ...route.query, sort, direction: 'asc' }
router.push({ query })
}
})
function getIcon(name: string) {
if (sortField.value[0] === name) {
return sortField.value[1] === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const UInput = resolveComponent('UInput') const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
function getIcon(name: string) {
if (sortField.value[0] === name) {
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
const updateFilter = debounce((columnId: string, val: string) => {
const newFilters = { ...filters.value }
if (val) newFilters[columnId] = val
else delete newFilters[columnId]
filters.value = newFilters
}, 400)
const columns: TableColumn<Product>[] = [ const columns: TableColumn<Product>[] = [
{
id: 'expand',
cell: ({ row }) =>
h(UButton, {
color: 'neutral',
variant: 'ghost',
icon: 'i-lucide-chevron-down',
square: true,
'aria-label': 'Expand',
ui: {
leadingIcon: [
'transition-transform',
row.getIsExpanded() ? 'duration-200 rotate-180' : ''
]
},
onClick: () => row.toggleExpanded()
})
},
{ {
accessorKey: 'product_id', accessorKey: 'product_id',
header: 'ID', header: ({ column }) => {
cell: ({ row }) => `#${row.getValue('product_id')}` return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['product_id', 'asc']
}
}, [
h('span', 'ID'),
h(UIcon, {
name: getIcon('product_id')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => `#${row.getValue('product_id') as number}`
}, },
{ {
accessorKey: 'image_link', accessorKey: 'image_link',
header: 'Image', header: 'Image',
cell: ({ row }) => cell: ({ row }) => {
h('img', { return h('img', {
src: row.getValue('image_link'), src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;', style: 'width:40px;height:40px;object-fit:cover;'
onError: (e: Event) => {
(e.target as HTMLImageElement).src = errorImg
}
}) })
}
}, },
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name', header: ({ column }) => {
cell: ({ row }) => row.getValue('name') return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['name', 'asc']
}
}, [
h('span', 'Name'),
h(UIcon, {
name: getIcon('name')
})
]),
h(UInput, {
placeholder: 'Search...',
modelValue: filters.value[column.id] ?? '',
'onUpdate:modelValue': (val: string) => {
updateFilter(column.id, val)
},
size: 'xs'
})
])
},
cell: ({ row }) => row.getValue('name') as string,
filterFn: (row, columnId, value) => {
const name = row.getValue(columnId) as string
return name.toLowerCase().includes(value.toLowerCase())
}
}, },
{ {
accessorKey: 'quantity', accessorKey: 'quantity',
header: 'In stock', header: ({ column }) => {
cell: ({ row }) => row.getValue('quantity') return h('div', { class: 'flex flex-col gap-1' }, [
h('div', {
class: 'flex items-center gap-2 cursor-pointer',
onClick: () => {
sortField.value = ['quantity', 'asc']
}
}, [
h('span', 'In stock'),
h(UIcon, {
name: getIcon('quantity')
})
]),
])
},
cell: ({ row }) => row.getValue('quantity') as number
}, },
{ {
accessorKey: 'action', accessorKey: 'count',
header: '', header: 'Count',
cell: ({ row }) => cell: ({ row }) => {
h( return h(UInputNumber, {
UButton, modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
{ 'onUpdate:modelValue': (val: number) => {
onClick: () => if (val)
router.push({ selectedCount.value = {
name: 'admin-product-details',
params: {
product_id: row.original.product_id, product_id: row.original.product_id,
link_rewrite: row.original.link_rewrite count: val
} }
}), else {
selectedCount.value = {
product_id: null,
count: 0
}
}
},
min: 0,
max: row.original.quantity
})
}
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
addToCart(row.original.product_id)
},
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
variant: 'solid'
}, 'Add to cart')
},
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
goToProduct(row.original.product_id)
},
class: 'cursor-pointer',
color: 'info', color: 'info',
variant: 'soft' variant: 'soft'
}, () => 'Show product')
}, },
() => 'Show product' },
) {
accessorKey: 'counta',
header: '',
cell: ({ row }) => {
return h(UIcon, {
onClick: () => toggleFavorite(row.original),
class: [
'cursor-pointer text-[20px] transition-transform duration-200 hover:scale-125',
row.original.is_favorite ? 'text-red-500' : 'text-blue-500'
],
name: 'material-symbols:favorite',
variant: 'soft',
})
}
} }
] ]
const columnsChild: TableColumn<Product>[] = [
{
accessorKey: 'product_id',
header: '',
cell: ({ row }) => `#${row.getValue('product_id') as number}`
},
{
accessorKey: 'image_link',
header: '',
cell: ({ row }) => {
return h('img', {
src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;'
})
}
},
{
accessorKey: 'name',
header: '',
cell: ({ row }) => row.getValue('name') as string
},
{
accessorKey: 'quantity',
header: '',
cell: ({ row }) => row.getValue('quantity') as number
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UInputNumber, {
modelValue: selectedCount.value.product_id === row.original.product_id ? selectedCount.value.count : 0,
'onUpdate:modelValue': (val: number) => {
if (val)
selectedCount.value = {
product_id: row.original.product_id,
count: val
}
else {
selectedCount.value = {
product_id: null,
count: 0
}
}
},
min: 0,
max: row.original.quantity
})
}
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
addToCart(row.original.product_id)
},
color: selectedCount.value.product_id !== row.original.product_id ? 'info' : 'primary',
disabled: selectedCount.value.product_id !== row.original.product_id,
variant: 'solid'
}, 'Add to cart')
},
},
{
accessorKey: 'count',
header: '',
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
goToProduct(row.original.product_id)
},
class: 'cursor-pointer',
color: 'info',
variant: 'soft'
}, () => 'Show product')
},
},
{
accessorKey: 'counta',
header: '',
cell: ({ row }) => {
return h(UIcon, {
onClick: () => toggleFavorite(row.original),
class: [
'cursor-pointer text-[20px] transition-transform duration-200 hover:scale-125',
row.original.is_favorite ? 'text-red-500' : 'text-blue-500'
],
name: 'material-symbols:favorite',
variant: 'soft',
})
}
}
]
import { useCustomerProductStore } from '@/stores/customer/customer-product'
const customerProductStore = useCustomerProductStore()
function toggleFavorite(product: Product) {
customerProductStore.toggleFavorite(product)
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -54,6 +54,7 @@ const locale = computed({
const pathParts = currentPath.split('/').filter(Boolean) const pathParts = currentPath.split('/').filter(Boolean)
cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' }) cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
if (pathParts.length > 0) { if (pathParts.length > 0) {
const isLocale = langs.some((l) => l.lang_code === pathParts[0]) const isLocale = langs.some((l) => l.lang_code === pathParts[0])
if (isLocale) { if (isLocale) {

View File

@@ -1,99 +0,0 @@
<template>
<div class="rich-editor rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden focus-within:ring-1 focus-within:ring-sky-500 focus-within:border-sky-500 transition-colors">
<!-- Toolbar -->
<div class="flex items-center gap-0.5 px-2 py-1.5 border-b border-(--border-light) dark:border-(--border-dark) bg-gray-50 dark:bg-neutral-800 flex-wrap">
<button type="button" @click="editor?.chain().focus().toggleBold().run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('bold') }"
class="toolbar-btn font-bold">B</button>
<button type="button" @click="editor?.chain().focus().toggleItalic().run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('italic') }"
class="toolbar-btn italic">I</button>
<button type="button" @click="editor?.chain().focus().toggleStrike().run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('strike') }"
class="toolbar-btn line-through">S</button>
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
<button type="button" @click="editor?.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('heading', { level: 2 }) }"
class="toolbar-btn text-xs font-semibold">H2</button>
<button type="button" @click="editor?.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('heading', { level: 3 }) }"
class="toolbar-btn text-xs font-semibold">H3</button>
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
<button type="button" @click="editor?.chain().focus().toggleBulletList().run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('bulletList') }"
class="toolbar-btn">
<UIcon name="i-lucide-list" class="text-sm" />
</button>
<button type="button" @click="editor?.chain().focus().toggleOrderedList().run()"
:class="{ 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-300': editor?.isActive('orderedList') }"
class="toolbar-btn">
<UIcon name="i-lucide-list-ordered" class="text-sm" />
</button>
<div class="w-px h-4 bg-gray-300 dark:bg-neutral-600 mx-1" />
<button type="button" @click="editor?.chain().focus().undo().run()" class="toolbar-btn">
<UIcon name="i-lucide-undo-2" class="text-sm" />
</button>
<button type="button" @click="editor?.chain().focus().redo().run()" class="toolbar-btn">
<UIcon name="i-lucide-redo-2" class="text-sm" />
</button>
</div>
<!-- Editor area -->
<EditorContent :editor="editor"
class="min-h-32 px-3 py-2.5 text-sm text-black dark:text-white bg-white dark:bg-neutral-900 prose prose-sm dark:prose-invert max-w-none focus:outline-none" />
</div>
</template>
<script setup lang="ts">
import { watch, onBeforeUnmount } from 'vue'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'
const props = defineProps<{
modelValue: string
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const editor = useEditor({
content: props.modelValue,
extensions: [
StarterKit,
Placeholder.configure({ placeholder: props.placeholder ?? '' }),
],
editorProps: {
attributes: { class: 'focus:outline-none' },
},
onUpdate({ editor }) {
emit('update:modelValue', editor.getHTML())
},
})
// Sync external changes (e.g. store reset)
watch(() => props.modelValue, (val) => {
if (editor.value && editor.value.getHTML() !== val) {
editor.value.commands.setContent(val, false)
}
})
onBeforeUnmount(() => editor.value?.destroy())
</script>
<style>
/* .toolbar-btn {
@apply flex items-center justify-center w-7 h-7 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-neutral-700 transition-colors text-sm;
} */
/* Tiptap placeholder */
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
/* color: theme('colors.gray.400'); */
pointer-events: none;
height: 0;
}
</style>

View File

@@ -0,0 +1,32 @@
import { ref, watch } from 'vue'
export function useDebouncedSearch<T>(fetchFn: () => Promise<void>, delay = 400) {
const loading = ref(false)
function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
let t: ReturnType<typeof setTimeout> | null = null
return (...args: Parameters<T>) => {
if (t) clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}
const debouncedFetch = debounce(fetchFn, delay)
watch(() => { }, () => {
loading.value = false
}, { immediate: true })
return {
loading,
debouncedFetch
}
}
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let t: ReturnType<typeof setTimeout> | null = null
return (...args: Parameters<T>) => {
if (t) clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
}
}

View File

@@ -0,0 +1,109 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useCartStore } from '@/stores/customer/cart'
import { useCustomerProductStore } from '@/stores/customer/customer-product'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useFetchJson } from '@/composable/useFetchJson'
export interface SearchProductParams {
query: string
searchByReference?: boolean
}
export function useProductSearchApi() {
const router = useRouter()
const cartStore = useCartStore()
const customerProductStore = useCustomerProductStore()
const toast = useToast()
const searchQuery = ref('')
const products = ref<any[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const searchByReference = ref(false)
const selectedCount = ref({
product_id: null as number | null,
count: 0
})
async function fetchProducts() {
if (!searchQuery.value.trim()) {
products.value = []
return
}
loading.value = true
error.value = null
try {
let query = ''
if (searchByReference.value) {
query = `reference_eq=${searchQuery.value.trim()}`
} else {
query = `name=~${searchQuery.value.trim()}`
}
const result = await useFetchJson(
`/api/v1/restricted/product/list?elems=30&${query}`
)
products.value = result.items || []
} catch (e) {
error.value = 'Failed to load products'
} finally {
loading.value = false
}
}
async function addToCart(product_id: number) {
if (!cartStore.activeCartId) {
toast.add({
title: "No cart selected",
description: "Please select a cart before adding products",
icon: "i-heroicons-exclamation-triangle",
duration: 5000
})
return
}
const count = selectedCount.value.count || 1
await cartStore.addProduct(product_id, count)
toast.add({
title: "Product added to cart",
description: `Quantity: ${count}`,
icon: "i-heroicons-check-circle",
duration: 5000
})
}
function goToProduct(productId: number) {
router.push({
name: 'customer-product-details',
params: { product_id: productId }
})
}
function getSortIcon(field: string, sortValue: [string | undefined, string | undefined]): string {
if (sortValue[0] === field) {
if (sortValue[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
if (sortValue[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
}
return 'i-lucide-arrow-up-down'
}
return {
searchQuery,
products,
loading,
error,
searchByReference,
selectedCount,
fetchProducts,
addToCart,
goToProduct,
getSortIcon
}
}

View File

@@ -0,0 +1,82 @@
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export function useTableState() {
const route = useRoute()
const router = useRouter()
const page = computed({
get: () => Number(route.query.page) || 1,
set: (val: number) => {
router.push({
query: {
...route.query,
page: val
}
})
}
})
const sortField = computed({
get: () => [
route.query.sort as string | undefined,
route.query.direction as 'asc' | 'desc' | undefined
],
set: ([sort]: [string, 'asc' | 'desc']) => {
const currentSort = route.query.sort as string | undefined
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
let query = { ...route.query }
if (currentSort === sort) {
if (currentDirection === 'asc') {
query.direction = 'desc'
} else if (currentDirection === 'desc') {
delete query.sort
delete query.direction
} else {
query.direction = 'asc'
query.sort = sort
}
} else {
query.sort = sort
query.direction = 'asc'
}
router.push({ query })
}
})
const filters = computed<Record<string, string>>({
get: () => {
const q = { ...route.query }
delete q.page
delete q.sort
delete q.direction
return q as Record<string, string>
},
set: (val) => {
const baseQuery = { ...route.query }
Object.keys(baseQuery).forEach(key => {
if (!['page', 'sort', 'direction'].includes(key)) {
delete baseQuery[key]
}
})
router.push({
query: {
...baseQuery,
...val,
page: 1
}
})
}
})
return {
page,
sortField,
filters
}
}

View File

@@ -205,7 +205,8 @@ const menu = ref<TopMenuItem[] | null>(null)
async function getTopMenu() { async function getTopMenu() {
try { try {
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu') const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
menu.value = items[0]?.children || []
menu.value = items
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }

View File

@@ -103,6 +103,8 @@ async function setRoutes() {
} }
}) })
} }
console.log(router);
// await router.replace(router.currentRoute.value.fullPath) // await router.replace(router.currentRoute.value.fullPath)
} }

View File

@@ -12,7 +12,10 @@ export const currentCountry = ref<Country>()
const defLang = ref<Language>() const defLang = ref<Language>()
const defCountry = ref<Country>() const defCountry = ref<Country>()
const cookie = useCookie() const cookie = useCookie()
// Get available language codes for route matching
// export const availableLocales = computed(() => langs.map((l) => l.lang_code))
// Initialize languages from API
export async function initLangs() { export async function initLangs() {
try { try {
const { items } = await useFetchJson<Language[]>('/api/v1/langs') const { items } = await useFetchJson<Language[]>('/api/v1/langs')
@@ -30,6 +33,8 @@ export async function initLangs() {
} }
} }
// Initialize country/currency from API
export async function initCountryCurrency() { export async function initCountryCurrency() {
try { try {
const { items } = await useFetchJson<Country[]>('/api/v1/restricted/langs-and-countries/get-countries') const { items } = await useFetchJson<Country[]>('/api/v1/restricted/langs-and-countries/get-countries')
@@ -38,10 +43,13 @@ export async function initCountryCurrency() {
let idfromcookie = null let idfromcookie = null
const cc = cookie.getCookie('country_id') const cc = cookie.getCookie('country_id')
if (cc) { if (cc) {
idfromcookie = countries.find((x) => x.id == parseInt(cc)) idfromcookie = langs.find((x) => x.id == parseInt(cc))
} }
defCountry.value = items.find((x) => x.id === defLang.value?.id) defCountry.value = items.find((x) => x.id === defLang.value?.id)
currentCountry.value = idfromcookie ?? defCountry.value currentCountry.value = idfromcookie ?? defCountry.value
console.log(defCountry.value);
console.log(currentCountry.value);
} catch (error) { } catch (error) {
console.error('Failed to fetch languages:', error) console.error('Failed to fetch languages:', error)
} }
@@ -52,6 +60,7 @@ export async function switchLocalization() {
await useFetchJson('/api/v1/public/auth/update-choice', { await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST' method: 'POST'
}) })
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }

View File

@@ -6,7 +6,7 @@ import { settings } from "./settings";
const categoryId = ref() const categoryId = ref()
export const getMenu = async () => { export const getMenu = async () => {
if (!categoryId.value) { if(!categoryId.value){
categoryId.value = settings['app'].category_tree_root_id categoryId.value = settings['app'].category_tree_root_id
} }
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${categoryId.value}`); const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${categoryId.value}`);

View File

@@ -1,164 +0,0 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
import type {
ProductCategory,
ProductRelated,
ProductForm,
ProductFormProduct,
ProductFormPrice,
ProductFormVariant,
ProductVariantForm,
ProductImage,
} from '@/types/product'
import type { MenuItem } from '@/types'
import { settings } from '@/router/settings'
// ── Default values ────────────────────────────────────────────────────────────
function emptyForm(): ProductForm {
return {
product: {
reference: '',
base_price: 0,
quantity: 0,
minimal_quantity: 1,
available_for_order: true,
available_date: '',
out_of_stock_behavior: 2,
on_sale: false,
show_price: true,
condition: 'new',
is_virtual: false,
weight: 0,
width: 0,
height: 0,
depth: 0,
delivery_days: null,
active: true,
visibility: 'both',
indexed: true,
date_add: '',
date_upd: '',
name: '',
description: '',
description_short: '',
manufacturer: '',
category: '',
is_favorite: false,
is_oem: false,
is_new: false,
},
price: {
base: 0,
final_tax_excl: 0,
final_tax_incl: 0,
tax_rate: 0,
priority: 0,
},
variants: [],
}
}
// ── Store ─────────────────────────────────────────────────────────────────────
export const useAddProductStore = defineStore('addProduct', () => {
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
const successMessage = ref<string | null>(null)
// Form data for creating / editing a product
const form = reactive<ProductForm>(emptyForm())
// Variants loaded when editing an existing product
const variants = ref<ProductVariantForm[]>([])
const variantSaving = ref<Record<number, boolean>>({})
const variantErrors = ref<Record<number, string>>({})
// Images
const images = ref<ProductImage[]>([])
// Categories & related products
const selectedCategories = ref<ProductCategory[]>([])
const relatedProducts = ref<ProductRelated[]>([])
// ── Product ─────────────────────────────────────────────────────────
async function loadProduct(productId: number) {
loading.value = true
error.value = null
try {
// const resp = await useFetchJson<ProductForm>(`/api/v1/restricted/admin/product/${productId}`)
// Object.assign(form, resp.items)
console.log('[addProduct] loadProduct API not connected yet', productId)
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load product'
} finally {
loading.value = false
}
}
// ── Images ────────────────────────────────────────────────────────────────
function addImageFiles(files: FileList | File[]) {
const incoming = Array.from(files)
for (const file of incoming) {
const previewUrl = URL.createObjectURL(file)
images.value.push({ previewUrl, file, cover: false, uploading: false })
}
if (!images.value.some(i => i.cover) && images.value.length > 0) {
images.value[0]!.cover = true
}
}
function removeImage(index: number) {
const img = images.value[index]!
if (img.previewUrl.startsWith('blob:')) URL.revokeObjectURL(img.previewUrl)
const wasCover = img.cover
images.value.splice(index, 1)
if (wasCover && images.value.length > 0) {
images.value[0]!.cover = true
}
}
function setCover(index: number) {
images.value.forEach((img, i) => { img.cover = i === index })
}
function resetForm() {
Object.assign(form, emptyForm())
variants.value = []
images.value = []
selectedCategories.value = []
relatedProducts.value = []
error.value = null
successMessage.value = null
}
const categories = ref<MenuItem[]>([])
async function loadCategories() {
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${settings['app'].category_tree_root_id}`);
categories.value = resp.items.children
}
return {
loading,
saving,
error,
successMessage,
form,
images,
selectedCategories,
relatedProducts,
variants,
variantSaving,
variantErrors,
categories,
loadProduct,
addImageFiles,
removeImage,
setCover,
resetForm,
loadCategories
}
})

View File

@@ -1,58 +1,231 @@
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref, computed } from 'vue'
import type { AddressTemplate } from './cart'
export interface AddressFormData {
street: string
zipCode: string
city: string
country: string
}
export interface Address { export interface Address {
id: number id: number
country_id: number street: string
address_unparsed: Record<string, string> zipCode: string
city: string
country: string
} }
export const useAddressStore = defineStore('address', () => { export const useAddressStore = defineStore('address', () => {
const addresses = ref<Address[]>([])
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
async function fetchAddresses() { const currentPage = ref(1)
loading.value = true const pageSize = 20
error.value = null
try { const searchQuery = ref('')
const res = await useFetchJson<Address[]>('/api/v1/restricted/addresses/retrieve-addresses')
addresses.value = res.items ?? [] function initMockData() {
} catch (e: unknown) { addresses.value = [
error.value = e instanceof Error ? e.message : 'Failed to load addresses' { id: 1, street: 'Main Street 123', zipCode: '10-001', city: 'New York', country: 'United States' },
} finally { { id: 2, street: 'Oak Avenue 123', zipCode: '90-001', city: 'Los Angeles', country: 'United States' },
loading.value = false { id: 3, street: 'Pine Road 123', zipCode: '60-601', city: 'Chicago', country: 'United States' }
} ]
} }
async function deleteAddress(id: number) { const filteredAddresses = computed(() => {
await useFetchJson(`/api/v1/restricted/addresses/delete-address?address_id=${id}`, { method: 'DELETE' }) if (!searchQuery.value) return addresses.value
addresses.value = addresses.value.filter((a) => a.id !== id)
}
async function getTemplate(countryId: number): Promise<Record<string, string>> { const query = searchQuery.value.toLowerCase()
const res = await useFetchJson<Record<string, string>>(
`/api/v1/restricted/addresses/get-template?country_id=${countryId}` return addresses.value.filter(addr =>
addr.street.toLowerCase().includes(query) ||
addr.city.toLowerCase().includes(query) ||
addr.country.toLowerCase().includes(query) ||
addr.zipCode.toLowerCase().includes(query)
) )
return res.items ?? {}
}
async function createAddress(countryId: number, data: Record<string, string>) {
await useFetchJson(`/api/v1/restricted/addresses/add-new-address?country_id=${countryId}`, {
method: 'POST',
body: JSON.stringify(data)
}) })
await fetchAddresses()
const totalItems = computed(() => filteredAddresses.value.length)
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
function getAddressById(id: number) {
return addresses.value.find(addr => addr.id === id)
} }
async function updateAddress(id: number, countryId: number, data: Record<string, string>) { function normalize(data: AddressFormData): AddressFormData {
await useFetchJson(`/api/v1/restricted/addresses/modify-address?country_id=${countryId}&address_id=${id}`, { return {
street: data.street.trim(),
zipCode: data.zipCode.trim(),
city: data.city.trim(),
country: data.country.trim()
}
}
function generateId(): number {
return Math.max(0, ...addresses.value.map(a => a.id)) + 1
}
function setPage(page: number) {
currentPage.value = page
}
function setSearchQuery(query: string) {
searchQuery.value = query
currentPage.value = 1
}
function resetPagination() {
currentPage.value = 1
}
initMockData()
const addresses = ref<Address[]>([])
const addressLoading = ref(false)
const addressError = ref<string | null>(null)
const addressSearchQuery = ref('')
const addressCurrentPage = ref(1)
const addressPageSize = 20
const addressTotalCount = ref(0)
function transformAddressResponse(address: any): Address {
const info = address.address_info || address.addressInfo || {}
return {
id: address.id ?? 0,
country_id: address.country_id ?? address.countryId ?? 1,
customer_id: address.customer_id ?? address.customerId ?? 0,
address_info: info as Record<string, string>
}
}
async function fetchAddresses() {
addressLoading.value = true
addressError.value = null
try {
const queryParam = addressSearchQuery.value ? `&query=${encodeURIComponent(addressSearchQuery.value)}` : ''
const response = await useFetchJson<Address[]>(`/api/v1/restricted/addresses/retrieve-addresses?page=${addressCurrentPage.value}&elems=${addressPageSize}${queryParam}`)
addresses.value = response.items || []
addressTotalCount.value = response.count ?? addresses.value.length
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to load addresses'
} finally {
addressLoading.value = false
}
}
async function addAddress(countryId: number, formData: AddressTemplate): Promise<Address | null> {
addressLoading.value = true
addressError.value = null
try {
const response = await useFetchJson<any>(`/api/v1/restricted/addresses/add-new-address?country_id=${countryId}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data) body: JSON.stringify(formData)
}) })
await fetchAddresses() await fetchAddresses()
return transformAddressResponse(response.items ?? response)
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to create address'
return null
} finally {
addressLoading.value = false
}
} }
return { addresses, loading, error, fetchAddresses, deleteAddress, getTemplate, createAddress, updateAddress } async function getAddressTemplate(countryId: number): Promise<AddressTemplate> {
const response = await useFetchJson<any>(`/api/v1/restricted/addresses/get-template?country_id=${countryId}`)
return response.items ?? {}
}
async function updateAddress(id: number, countryId: number, formData: AddressTemplate): Promise<boolean> {
addressLoading.value = true
addressError.value = null
try {
await useFetchJson<any>(`/api/v1/restricted/addresses/modify-address?country_id=${countryId}&address_id=${id}`, {
method: 'POST',
body: JSON.stringify(formData)
})
await fetchAddresses()
return true
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to update address'
return false
} finally {
addressLoading.value = false
}
}
async function deleteAddress(id: number): Promise<boolean> {
addressLoading.value = true
addressError.value = null
try {
await useFetchJson<any>(`/api/v1/restricted/addresses/delete-address?address_id=${id}`, {
method: 'DELETE'
})
await fetchAddresses()
return true
} catch (error: unknown) {
addressError.value = error instanceof Error ? error.message : 'Failed to delete address'
return false
} finally {
addressLoading.value = false
}
}
const totalAddressItems = computed(() => addressTotalCount.value || addresses.value.length)
const totalAddressPages = computed(() => Math.ceil(totalAddressItems.value / addressPageSize))
const paginatedAddresses = computed(() => addresses.value)
function setAddressPage(page: number) {
addressCurrentPage.value = page
return fetchAddresses()
}
function setAddressSearchQuery(query: string) {
addressSearchQuery.value = query
addressCurrentPage.value = 1
return fetchAddresses()
}
return {
addresses,
loading,
error,
currentPage,
pageSize,
totalItems,
totalPages,
searchQuery,
filteredAddresses,
paginatedAddresses,
getAddressById,
addAddress,
updateAddress,
deleteAddress,
setPage,
setSearchQuery,
resetPagination,
addressLoading,
addressError,
addressSearchQuery,
addressCurrentPage,
addressPageSize,
totalAddressItems,
totalAddressPages,
fetchAddresses,
setAddressPage,
setAddressSearchQuery,
getAddressTemplate
}
}) })

View File

@@ -75,7 +75,7 @@ async function addNewCart(name: string) {
try { try {
const res = await useFetchJson<ApiResponse>( const res = await useFetchJson<ApiResponse>(
`/api/v1/restricted/carts/add-product-to-cart?cart_id=${activeCartId.value}&product_id=${product_id}&amount=${count}` `/api/v1/restricted/carts/add-product-to-cart?cart_id=${activeCartId.value}&product_id=${product_id}&amount=${count}&set_amount=false`
) )
console.log('fsdfsdfdsfdsfs', res) console.log('fsdfsdfdsfdsfs', res)

View File

@@ -33,30 +33,26 @@ export const useCustomerProductStore = defineStore('customer-product', () => {
const total = ref(0) const total = ref(0)
const perPage = ref(15) const perPage = ref(15)
async function fetchProductList() { async function fetchProductList(paramsObj: Record<string, any>) {
loading.value = true loading.value = true
error.value = null error.value = null
const params = new URLSearchParams() const params = new URLSearchParams()
Object.entries(route.query).forEach(([key, value]) => { Object.entries(paramsObj).forEach(([key, value]) => {
if (value) params.append(key, String(value)) if (value) params.append(key, String(value))
}) })
const url = `/api/v1/restricted/product/list?${params}` const url = `/api/v1/restricted/product/list?${params}`
try { try {
const response = await useFetchJson<ApiResponse>(url) const response = await useFetchJson<ApiResponse>(url)
productsList.value = response.items || [] productsList.value = response.items || []
total.value = response.count || 0 total.value = response.count || 0
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load products'
} finally { } finally {
loading.value = false loading.value = false
} }
} }
function updateFavoriteState(product_id: number, value: boolean) { function updateFavoriteState(product_id: number, value: boolean) {
const p = productsList.value.find(p => p.product_id === product_id) const p = productsList.value.find(p => p.product_id === product_id)
if (p) p.is_favorite = value if (p) p.is_favorite = value

View File

@@ -18,93 +18,3 @@ export interface Product {
image_link: string image_link: string
link_rewrite: string link_rewrite: string
} }
export interface ProductCategory {
id_category: number
name: string
}
export interface ProductRelated {
id_product: number
name: string
reference: string
}
export interface ProductFormProduct {
id?: number
reference: string
base_price: number
quantity: number
minimal_quantity: number
available_for_order: boolean
available_date: string
out_of_stock_behavior: number // 0=deny, 1=allow, 2=default
on_sale: boolean
show_price: boolean
condition: 'new' | 'used' | 'refurbished'
is_virtual: boolean
weight: number
width: number
height: number
depth: number
delivery_days: number | null
active: boolean
visibility: 'both' | 'catalog' | 'search' | 'none'
indexed: boolean
date_add: string
date_upd: string
name: string
description: string
description_short: string
manufacturer: string
category: string
is_favorite: boolean
is_oem: boolean
is_new: boolean
}
export interface ProductFormPrice {
base: number
final_tax_excl: number
final_tax_incl: number
tax_rate: number
priority: number
}
export interface ProductFormVariantAttribute {
group: string
attribute: string
}
export interface ProductFormVariant {
id_product_attribute: number
reference: string
base_price: number
price_tax_excl: number
price_tax_incl: number
quantity: number
attributes: ProductFormVariantAttribute[]
}
export type ProductVariantForm = ProductFormVariant
export interface ProductForm {
product: ProductFormProduct
price: ProductFormPrice
variants: ProductFormVariant[]
}
export interface ProductImage {
/** Temporary local URL for preview before upload */
previewUrl: string
/** File object present only before upload */
file?: File
/** Server-side image id after upload */
id_image?: number
/** Whether this is the cover image */
cover: boolean
/** Upload in progress */
uploading?: boolean
/** Upload error */
error?: string
}

View File

@@ -1,7 +1,7 @@
info: info:
name: Delete Index - MeiliSearch name: Delete Index - MeiliSearch
type: http type: http
seq: 8 seq: 7
http: http:
method: DELETE method: DELETE

View File

@@ -1,19 +0,0 @@
info:
name: Add new cart
type: http
seq: 1
http:
method: POST
url: "{{bas_url}}/restricted/carts/add-new-cart?name=TestCart"
params:
- name: name
value: TestCart
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,31 +0,0 @@
info:
name: Add product to cart
type: http
seq: 6
http:
method: POST
url: "{{bas_url}}/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=1&set_amount=true"
params:
- name: cart_id
value: "1"
type: query
- name: product_id
value: "51"
type: query
- name: product_attribute_id
value: "1115"
type: query
- name: amount
value: "1"
type: query
- name: set_amount
value: "true"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,22 +0,0 @@
info:
name: Change cart name
type: http
seq: 3
http:
method: PATCH
url: "{{bas_url}}/restricted/carts/change-cart-name?cart_id=1&new_name=UpdatedCart"
params:
- name: cart_id
value: "1"
type: query
- name: new_name
value: UpdatedCart
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,19 +0,0 @@
info:
name: Remove cart
type: http
seq: 2
http:
method: DELETE
url: "{{bas_url}}/restricted/carts/remove-cart?cart_id=1"
params:
- name: cart_id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,25 +0,0 @@
info:
name: Remove product from cart
type: http
seq: 7
http:
method: DELETE
url: "{{bas_url}}/restricted/carts/remove-product-from-cart?cart_id=1&product_id=51&product_attribute_id=1115"
params:
- name: cart_id
value: "1"
type: query
- name: product_id
value: "51"
type: query
- name: product_attribute_id
value: "1115"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,19 +0,0 @@
info:
name: Retrieve cart
type: http
seq: 5
http:
method: GET
url: "{{bas_url}}/restricted/carts/retrieve-cart?cart_id=1"
params:
- name: cart_id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,15 +0,0 @@
info:
name: Retrieve carts info
type: http
seq: 4
http:
method: GET
url: "{{bas_url}}/restricted/carts/retrieve-carts-info"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +0,0 @@
info:
name: cart
type: folder
seq: 7
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: currency name: currency
type: folder type: folder
seq: 10 seq: 9
request: request:
auth: inherit auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: customer name: customer
type: folder type: folder
seq: 11 seq: 10
request: request:
auth: inherit auth: inherit

View File

@@ -1,22 +0,0 @@
info:
name: Breadcrumb
type: http
seq: 1
http:
method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-breadcrumb?root_category_id=2&category_id=13
params:
- name: root_category_id
value: "2"
type: query
- name: category_id
value: "13"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,19 +0,0 @@
info:
name: Category tree
type: http
seq: 2
http:
method: GET
url: http://localhost:3000/api/v1/restricted/menu/get-category-tree?root_category_id=2
params:
- name: root_category_id
value: "2"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,15 +0,0 @@
info:
name: Top Customer Management Menu
type: http
seq: 4
http:
method: GET
url: "{{bas_url}}/restricted/menu/get-customer-management-menu"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,15 +0,0 @@
info:
name: Top Menu
type: http
seq: 3
http:
method: GET
url: "{{bas_url}}/restricted/menu/get-top-menu"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +0,0 @@
info:
name: menu
type: folder
seq: 12
request:
auth: inherit

View File

@@ -1,33 +0,0 @@
info:
name: Change order address
type: http
seq: 3
http:
method: POST
url: "{{bas_url}}/restricted/orders/change-order-address?order_id=1&country_id=1"
params:
- name: order_id
value: "1"
type: query
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "śląskie",
"street": "Długa",
"building_no": "5",
"recipient": "Adam Adamowicz"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,22 +0,0 @@
info:
name: Change order status
type: http
seq: 1
http:
method: PATCH
url: "{{bas_url}}/restricted/orders/change-order-status?order_id=2&status=PENDING"
params:
- name: order_id
value: "2"
type: query
- name: status
value: PENDING
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,25 +0,0 @@
info:
name: List
type: http
seq: 1
http:
method: GET
url: "{{bas_url}}/restricted/orders/list?p=1&elems=30&sort=order_id,desc"
params:
- name: p
value: "1"
type: query
- name: elems
value: "30"
type: query
- name: sort
value: order_id,desc
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,37 +0,0 @@
info:
name: Place new order
type: http
seq: 2
http:
method: POST
url: "{{bas_url}}/restricted/orders/place-new-order?cart_id=1&name=Test+Order&country_id=1"
params:
- name: cart_id
value: "1"
type: query
- name: name
value: Test Order
type: query
- name: country_id
value: "1"
type: query
body:
type: json
data: |-
{
"postal_code": "31-154",
"city": "Kraków",
"voivodeship": "małopolskie",
"street": "Długa",
"building_no": "5",
"apartment_no": "7",
"recipient": "Jan Kowalski"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -1,7 +0,0 @@
info:
name: order
type: folder
seq: 13
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: product name: product
type: folder type: folder
seq: 9 seq: 8
request: request:
auth: inherit auth: inherit

View File

@@ -1,7 +1,7 @@
info: info:
name: routes name: routes
type: folder type: folder
seq: 12 seq: 10
request: request:
auth: inherit auth: inherit

View File

@@ -5,14 +5,15 @@ info:
http: http:
method: POST method: POST
url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=1&country_id=1 url: http://localhost:3000/api/v1/public/auth/update-choice?lang_id=0&country_id=1
params: params:
- name: lang_id - name: lang_id
value: "1" value: "0"
type: query type: query
- name: country_id - name: country_id
value: "1" value: "1"
type: query type: query
auth: inherit
settings: settings:
encodeUrl: true encodeUrl: true

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=2&set_amount=true url: http://localhost:3000/api/v1/restricted/carts/add-product-to-cart?cart_id=1&product_id=51&product_attribute_id=1115&amount=1&set_amount=true
params: params:
- name: cart_id - name: cart_id
value: "1" value: "1"
@@ -17,7 +17,7 @@ http:
value: "1115" value: "1115"
type: query type: query
- name: amount - name: amount
value: "2" value: "1"
type: query type: query
- name: set_amount - name: set_amount
value: "true" value: "true"

View File

@@ -56,8 +56,7 @@ INSERT IGNORE INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `act
', 1), ', 1),
(17, 'customer-management-profile', ':user_id/profile', '/components/customer-management/Profile.vue', '{ (17, 'customer-management-profile', ':user_id/profile', '/components/customer-management/Profile.vue', '{
"guest":true, "guest":true,
"name": "Profile", "name": "Profile"
"layout": "management"
} }
', 1), ', 1),
(18, 'admin-users-search', 'users-search', '/components/admin/UsersSearch.vue', '{ (18, 'admin-users-search', 'users-search', '/components/admin/UsersSearch.vue', '{
@@ -72,8 +71,7 @@ INSERT IGNORE INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `act
(20, 'customer-products', 'products', '/components/customer/PageProducts.vue', '{ "guest":true, "name": "Products" }', 1), (20, 'customer-products', 'products', '/components/customer/PageProducts.vue', '{ "guest":true, "name": "Products" }', 1),
(21, 'customer-product-details', 'products/:product_id', '/components/customer/PageProduct.vue', '{ "guest":true, "name": "Products" }', 1), (21, 'customer-product-details', 'products/:product_id', '/components/customer/PageProduct.vue', '{ "guest":true, "name": "Products" }', 1),
(22, 'customer-page-carts', 'carts', '/components/customer/PageCarts.vue', '{ "guest":true, "name": "Carts" }', 1), (22, 'customer-page-carts', 'carts', '/components/customer/PageCarts.vue', '{ "guest":true, "name": "Carts" }', 1),
(24, 'customer-search-products', 'search-products', '/components/customer/PageSearchProducts.vue', '{ "guest":true, "name": "Carts" }', 1), (24, 'customer-search-products', 'search-products', '/components/customer/PageSearchProducts.vue', '{ "guest":true, "name": "Carts" }', 1);
(25, 'admin-add-product', 'add-product', '/components/admin/AddProduct.vue', '{ "guest":true, "name": "Add Product" }', 1);
CREATE TABLE IF NOT EXISTS b2b_top_menu ( CREATE TABLE IF NOT EXISTS b2b_top_menu (
menu_id INT AUTO_INCREMENT NOT NULL, menu_id INT AUTO_INCREMENT NOT NULL,
@@ -311,51 +309,11 @@ INSERT IGNORE INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `a
"locale": "" "locale": ""
} }
} }
}', 1, 1),
(18, '{
"name": "admin-add-product",
"trans": {
"pl": {
"label": "Dodaj produkt"
},
"en": {
"label": "Add Product"
},
"de": {
"label": "Produkt hinzufügen"
}
},
"icon": "proicons:cart1"
}', 1, '{
"route": {
"name": "admin-add-product",
"params": {
"locale": ""
}
}
}', 1, 1); }', 1, 1);
CREATE TABLE IF NOT EXISTS b2b_customer_management_menu (
menu_id INT AUTO_INCREMENT NOT NULL,
label LONGTEXT NOT NULL DEFAULT '{}',
parent_id INT NULL DEFAULT NULL,
params LONGTEXT NOT NULL DEFAULT '{}',
active TINYINT NOT NULL DEFAULT 1,
position INT NOT NULL DEFAULT 1,
PRIMARY KEY (menu_id),
CONSTRAINT FK_b2b_customer_management_menu_parent_id FOREIGN KEY (parent_id)
REFERENCES b2b_customer_management_menu (menu_id)
ON DELETE RESTRICT ON UPDATE RESTRICT,
INDEX FK_b2b_customer_management_menu_parent_id_idx (parent_id ASC)
) ENGINE = InnoDB;
INSERT IGNORE INTO `b2b_customer_management_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES
(1, JSON_COMPACT('{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}'),NULL,JSON_COMPACT('{}'),1,1),
(3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,JSON_COMPACT('{}'),1,1),
(9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,JSON_COMPACT('{"route": {"name": "home", "params":{"locale": ""}}}'),1,1);
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_routes; DROP TABLE IF EXISTS b2b_routes;
DROP TABLE IF EXISTS b2b_top_menu; DROP TABLE IF EXISTS b2b_top_menu;
DROP TABLE IF EXISTS b2b_customer_management_menu;
DROP FUNCTION IF EXISTS `slugify_eu`; DROP FUNCTION IF EXISTS `slugify_eu`;

View File

@@ -113,7 +113,7 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
created_at DATETIME(6) NULL, created_at DATETIME(6) NULL,
updated_at DATETIME(6) NULL, updated_at DATETIME(6) NULL,
deleted_at DATETIME(6) NULL, deleted_at DATETIME(6) NULL,
is_no_vat TINYINT(1) NOT NULL DEFAULT 0 is_no_vat TINYINT(1) NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_email CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_email
@@ -251,11 +251,6 @@ CREATE TABLE IF NOT EXISTS b2b_customer_orders (
country_id BIGINT UNSIGNED NOT NULL, country_id BIGINT UNSIGNED NOT NULL,
address_string TEXT NOT NULL, address_string TEXT NOT NULL,
status VARCHAR(50) NOT NULL, status VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
base_price DECIMAL(10, 2) NOT NULL,
tax_incl DECIMAL(10, 2) NOT NULL,
tax_excl DECIMAL(10, 2) NOT NULL,
CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE, CONSTRAINT fk_customer_orders_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE NO ACTION ON UPDATE CASCADE,
CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE CONSTRAINT fk_customer_orders_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -462,30 +457,6 @@ END$$
DELIMITER ; DELIMITER ;
CREATE TABLE b2b_order_status_history (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
old_status VARCHAR(50) NULL,
new_status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
user_id BIGINT UNSIGNED NULL
);
CREATE INDEX idx_order_status_history_order
ON b2b_order_status_history(order_id);
CREATE INDEX idx_order_status_history_user
ON b2b_order_status_history(user_id);
ALTER TABLE b2b_order_status_history
ADD CONSTRAINT fk_order
FOREIGN KEY (order_id) REFERENCES b2b_customer_orders(order_id);
ALTER TABLE b2b_order_status_history
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES b2b_customers(id);
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_addresses; DROP TABLE IF EXISTS b2b_addresses;

View File

@@ -12,27 +12,6 @@ INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('admin','2');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3'); INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('unlogged','4'); INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('unlogged','4');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (1, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (4, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (5, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (6, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (10, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (15, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (16, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (1, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (2, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (12, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (13, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (14, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (1, '3');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (2, '3');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (12, '3');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (13, '3');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (14, '3');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (17, '1');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (18, '2');
INSERT INTO `b2b_top_menu_roles` (`top_menu_id`, `role_id`) VALUES (18, '3');
-- insert sample admin user admin@ma-al.com/Maal12345678 -- insert sample admin user admin@ma-al.com/Maal12345678
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role_id, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at) INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role_id, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
VALUES VALUES
@@ -129,15 +108,12 @@ INSERT INTO `b2b_route_roles` (`route_id`, `role_id`) VALUES
(15, '3'), (15, '3'),
(16, '2'), (16, '2'),
(16, '3'), (16, '3'),
(17, '2'), (17, '1'),
(17, '3'),
(18, '2'), (18, '2'),
(18, '3'), (18, '3'),
(19, '1'), (19, '1'),
(20, '1'), (20, '1'),
(21, '1'), (21, '1'),
(22, '1'), (22, '1'),
(24, '1'), (24, '1');
(25, '2'),
(25, '3');
-- +goose Down -- +goose Down

View File

@@ -415,7 +415,7 @@ BEGIN
LEFT JOIN ps_manufacturer m LEFT JOIN ps_manufacturer m
ON m.id_manufacturer = p.id_manufacturer ON m.id_manufacturer = p.id_manufacturer
LEFT JOIN ps_configuration LEFT JOIN ps_configuration
ON ps_configuration.name = 'PS_NB_DAYS_NEW_PRODUCT' ON ps_configuration.name = PS_NB_DAYS_NEW_PRODUCT
WHERE p.id_product = p_id_product WHERE p.id_product = p_id_product
LIMIT 1; LIMIT 1;