1 Commits

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

View File

@@ -28,7 +28,7 @@ tmp_dir = "tmp"
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
stop_on_error = true
[color]
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
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"
"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/service/langsService"
"git.ma-al.com/goc_daniel/b2b/app/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/version"
"github.com/spf13/cobra"
)
@@ -24,8 +21,6 @@ var (
return
}
logger.Init("b2b", nil, config.Get().Log.LogLevel, config.Get().Log.LogColorize)
// Create and setup the server
server := web.New()

View File

@@ -28,7 +28,6 @@ type Config struct {
Cors CorsConfig
MeiliSearch MeiliSearchConfig
Storage StorageConfig
Log LogConfig
}
type I18n struct {
@@ -88,11 +87,6 @@ type AppConfig struct {
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 {
SMTPHost string `env:"EMAIL_SMTP_HOST,localhost"`
SMTPPort int `env:"EMAIL_SMTP_PORT,587"`
@@ -215,11 +209,6 @@ func load() *Config {
if err != nil {
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)
return cfg

View File

@@ -1,6 +1,7 @@
package public
import (
"log"
"strconv"
"time"
@@ -10,7 +11,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/service/authService"
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/logger"
"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/responseErrors"
@@ -178,13 +178,7 @@ func (h *AuthHandler) ForgotPassword(c fiber.Ctx) error {
// Request password reset - always return success to prevent email enumeration
err := h.authService.RequestPasswordReset(req.Email)
if err != nil {
logger.Warn("password reset request failed",
"handler", "AuthHandler.ForgotPassword",
"email", req.Email,
"error", err.Error(),
)
log.Printf("Password reset request error: %v", err)
}
return c.JSON(fiber.Map{
@@ -313,6 +307,7 @@ func (h *AuthHandler) Register(c fiber.Ctx) error {
// Attempt registration
err := h.authService.Register(&req)
if err != nil {
log.Printf("Register error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": responseErrors.GetErrorCode(c, err),
})
@@ -452,6 +447,7 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
if err != nil {
log.Printf("Google OAuth callback error: %v", err)
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"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/utils/i18n"
"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/response"
"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))
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)).
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))
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)).
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))
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)).
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)
if err != nil {
logger.Error("failed to retrieve addresses",
"handler", "AddressesHandler.RetrieveAddressesInfo",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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))
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)).
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/utils/i18n"
"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/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -29,12 +28,12 @@ func NewCartsHandler() *CartsHandler {
func CartsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCartsHandler()
r.Post("/add-new-cart", handler.AddNewCart)
r.Get("/add-new-cart", handler.AddNewCart)
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-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)
return r
@@ -50,13 +49,6 @@ func (h *CartsHandler) AddNewCart(c fiber.Ctx) error {
name := c.Query("name")
new_cart, err := h.cartsService.CreateNewCart(userID, name)
if err != nil {
logger.Error("failed to create cart",
"handler", "CartsHandler.AddNewCart",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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))
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)).
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)
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)).
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)
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)).
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))
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)).
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)
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)).
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)
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)).
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/service/currencyService"
"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/response"
"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)
if err != nil {
logger.Error("failed to create currency rate",
"handler", "CurrencyHandler.PostCurrencyRate",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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))
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)))
}

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/utils/i18n"
"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/query/query_params"
"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)
if err != nil {
logger.Error("failed to get customer",
"handler", "customerHandler.customerData",
"customer_id", customerId,
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(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)
if err != nil {
logger.Error("failed to list customers",
"handler", "customerHandler.listCustomers",
"error", err.Error(),
)
return fc.Status(responseErrors.GetErrorStatus(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 {
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)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
}

View File

@@ -3,7 +3,6 @@ package restricted
import (
"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/logger"
"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/responseErrors"
@@ -35,12 +34,6 @@ func LocaleSelectorHandlerRoutes(r fiber.Router) fiber.Router {
func (h *LocaleSelectorHandler) GetLanguages(c fiber.Ctx) error {
languages, err := h.localeSelectorService.GetLanguages()
if err != nil {
logger.Error("failed to get languages",
"handler", "LocaleSelectorHandler.GetLanguages",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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 {
countries, err := h.localeSelectorService.GetCountriesAndCurrencies()
if err != nil {
logger.Error("failed to get countries",
"handler", "LocaleSelectorHandler.GetCountries",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}

View File

@@ -3,12 +3,9 @@ package restricted
import (
"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/utils/i18n"
"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/response"
"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-breadcrumb", handler.GetBreadcrumb)
r.Get("/get-top-menu", handler.GetTopMenu)
r.Get("/get-customer-management-menu", middleware.Require(perms.UserReadAny), handler.GetCustomerManagementMenu)
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)
if err != nil {
logger.Error("failed to get category tree",
"handler", "MenuHandler.GetCategoryTree",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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)
if err != nil {
logger.Error("failed to get breadcrumb",
"handler", "MenuHandler.GetBreadcrumb",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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)))
}
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 {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -2,16 +2,11 @@ package restricted
import (
"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/enums"
"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/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/query/query_params"
"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.Post("/place-new-order", handler.PlaceNewOrder)
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
}
@@ -58,12 +53,6 @@ func (h *OrdersHandler) ListOrders(c fiber.Ctx) error {
list, err := h.ordersService.Find(user, paging, filters)
if err != nil {
logger.Error("failed to list orders",
"handler", "OrdersHandler.ListOrders",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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",
"country_id": "b2b_customer_orders.country_id",
"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 {
@@ -113,21 +97,8 @@ func (h *OrdersHandler) PlaceNewOrder(c fiber.Ctx) error {
name := c.Query("name")
originalUserId, ok := localeExtractor.GetOriginalUserID(c)
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)
err = h.ordersService.PlaceNewOrder(userID, uint(cart_id), name, uint(country_id), address_info)
if err != nil {
logger.Error("failed to place order",
"handler", "OrdersHandler.PlaceNewOrder",
"user_id", userID,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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)
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)).
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.
// TODO: well, permissions and all that.
func (h *OrdersHandler) ChangeOrderStatus(c fiber.Ctx) error {
userId, ok := localeExtractor.GetUserID(c)
user, ok := localeExtractor.GetCustomer(c)
if !ok {
return c.Status(responseErrors.GetErrorStatus(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 {
return c.Status(responseErrors.GetErrorStatus(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 {
logger.Error("failed to change order status",
"handler", "OrdersHandler.ChangeOrderStatus",
"order_id", order_id,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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"
"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/logger"
"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/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))
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)).
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)
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)).
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))
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)).
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))
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)).
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)
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)).
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/utils/i18n"
"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/response"
"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))
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)).
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)
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)).
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)
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)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}

View File

@@ -2,6 +2,7 @@ package restricted
import (
"encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
"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"
"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/logger"
"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/responseErrors"
@@ -47,13 +47,7 @@ func (h *MeiliSearchHandler) CreateIndex(c fiber.Ctx) error {
err := h.meiliService.CreateIndex(id_lang)
if err != nil {
logger.Error("failed to create search index",
"handler", "MeiliSearchHandler.CreateIndex",
"lang_id", id_lang,
"error", err.Error(),
)
fmt.Printf("CreateIndex error: %v\n", err)
return c.Status(responseErrors.GetErrorStatus(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)
if err != nil {
logger.Error("failed to search",
"handler", "MeiliSearchHandler.Search",
"index", index,
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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 {
result, err = h.searchService.Search(index, c.Body(), id_lang)
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)).
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)
if err != nil {
logger.Error("failed to get index settings",
"handler", "MeiliSearchHandler.GetSettings",
"index", index,
"error", err.Error(),
)
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/model"
"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/logger"
"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/responseErrors"
@@ -52,12 +51,6 @@ func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
result, err := h.SpecificPriceService.Create(c.Context(), &pr)
if err != nil {
logger.Error("failed to create specific price",
"handler", "SpecificPriceHandler.Create",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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)
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)).
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 {
result, err := h.SpecificPriceService.List(c.Context())
if err != nil {
logger.Error("failed to list specific prices",
"handler", "SpecificPriceHandler.List",
"error", err.Error(),
)
return c.Status(responseErrors.GetErrorStatus(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)
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)).
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)
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)).
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)
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)).
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)
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)).
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/utils/i18n"
"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/response"
"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)
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)).
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)
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)).
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)
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)).
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
import (
"time"
"git.ma-al.com/goc_daniel/b2b/app/model/enums"
)
type CustomerOrder struct {
OrderID uint `gorm:"column:order_id;primaryKey;autoIncrement" json:"order_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"`
AddressString string `gorm:"column:address_string;not null" json:"address_string"`
AddressUnparsed *AddressUnparsed `gorm:"-" json:"address_unparsed"`
Status enums.OrderStatus `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"`
Status string `gorm:"column:status;size:50;not null" json:"status"`
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"
type B2BMenu struct {
type B2BTopMenu struct {
MenuID int `gorm:"column:menu_id;primaryKey;autoIncrement" json:"menu_id"`
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"`
@@ -10,22 +10,10 @@ type B2BMenu struct {
Active int8 `gorm:"column:active;type:tinyint;not null;default:1" json:"active"`
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"`
Children []*B2BMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
type B2BTopMenu struct {
B2BMenu
Parent *B2BTopMenu `gorm:"foreignKey:ParentID;references:MenuID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"parent,omitempty"`
Children []*B2BTopMenu `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}
func (B2BTopMenu) TableName() string {
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
import (
"time"
"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/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/find"
)
type UIOrdersRepo interface {
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)
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
ChangeOrderStatus(orderId uint, newStatus enums.OrderStatus, userId uint) error
GetOrderStatus(orderID uint) (enums.OrderStatus, error)
ChangeOrderStatus(order_id uint, status string) error
}
type OrdersRepo struct{}
@@ -39,18 +35,6 @@ func (repo *OrdersRepo) UserHasOrder(user_id uint, order_id uint) (bool, error)
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) {
var list []model.CustomerOrder
var total int64
@@ -85,13 +69,13 @@ func (repo *OrdersRepo) Find(user_id uint, p find.Paging, filt *filters.FiltersL
}, 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{
UserID: cart.UserID,
Name: name,
CountryID: country_id,
AddressString: address_info,
Status: enums.OrderStatusPending,
Status: constdata.NEW_ORDER_STATUS,
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,
})
}
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
if err != nil {
tx.Rollback()
return nil, err
}
err = tx.Commit().Error
if err != nil {
return nil, err
}
return &order, nil
return db.DB.Create(&order).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{}{
"country_id": country_id,
"address_string": address_info,
"updated_at": time.Now(),
}).
Error
}
func (repo *OrdersRepo) ChangeOrderStatus(orderID uint, newStatus enums.OrderStatus, userId uint) error {
tx := db.Get().Begin()
var currentStatus enums.OrderStatus
err := tx.Table("b2b_customer_orders").
Select("status").
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
func (repo *OrdersRepo) ChangeOrderStatus(order_id uint, status string) error {
return db.DB.
Table("b2b_customer_orders").
Where("order_id = ?", order_id).
Update("status", status).
Error
}

View File

@@ -9,7 +9,6 @@ import (
type UIRoutesRepo interface {
GetRoutes(langId uint, roleId uint) ([]model.Route, error)
GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
GetCustomerManagementMenu(langId uint) ([]model.B2BCustomerManagementMenu, error)
}
type RoutesRepo struct{}
@@ -39,23 +38,10 @@ func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, e
Get().
Model(model.B2BTopMenu{}).
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).
Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC").
Find(&menus).Error
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"
"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/responseErrors"
"github.com/dlclark/regexp2"
@@ -69,47 +68,22 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
// Find user by email
if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
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
}
logger.Error("login failed - database error",
"service", "AuthService.Login",
"email", req.Email,
"error", err.Error(),
)
return nil, "", fmt.Errorf("database error: %w", err)
}
// Check if user is active
if !user.IsActive {
logger.Info("login failed - user inactive",
"service", "AuthService.Login",
"email", req.Email,
"reason", "user account is inactive",
)
return nil, "", responseErrors.ErrUserInactive
}
// Check if email is verified
if !user.EmailVerified {
logger.Info("login failed - email not verified",
"service", "AuthService.Login",
"email", req.Email,
"reason", "email not verified",
)
return nil, "", responseErrors.ErrEmailNotVerified
}
// Verify password
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
}
@@ -120,38 +94,22 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
if req.LangID != nil {
_, err := s.GetLangISOCode(*req.LangID)
if err != nil {
logger.Warn("login failed - invalid language ID",
"service", "AuthService.Login",
"email", req.Email,
"reason", "invalid language ID",
)
return nil, "", responseErrors.ErrBadLangID
}
user.LangID = *req.LangID
}
user.Country = nil
s.db.Save(&user)
// Generate access token (JWT)
accessToken, err := s.generateAccessToken(&user)
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)
}
// Generate opaque refresh token and store in DB
rawRefreshToken, err := s.createRefreshToken(user.ID)
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)
}
@@ -212,11 +170,6 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
}
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)
}
@@ -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 {
logger.Warn("failed to send verification email",
"service", "AuthService.Register",
"email", req.Email,
"error", err.Error(),
)
// Log error but don't fail registration - user can request resend
_ = err
}
return nil
@@ -260,7 +210,6 @@ func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationReques
user.EmailVerificationToken = ""
user.EmailVerificationExpires = nil
user.Country = nil
if err := s.db.Save(&user).Error; err != nil {
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.PasswordResetExpires = &expiresAt
user.LastPasswordResetRequest = &now
user.Country = nil
if err := s.db.Save(&user).Error; err != nil {
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) {
return responseErrors.ErrInvalidResetToken
}
logger.Error("password reset failed - database error",
"service", "AuthService.ResetPassword",
"error", err.Error(),
)
return fmt.Errorf("database error: %w", err)
}
@@ -384,12 +328,7 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
user.PasswordResetToken = ""
user.PasswordResetExpires = nil
user.Country = 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)
}
@@ -600,7 +539,6 @@ func (s *AuthService) UpdateJWTToken(user *model.Customer) (string, error) {
}
// Save the updated user
user.Country = nil
if err := s.db.Save(user).Error; err != nil {
return "", fmt.Errorf("database error: %w", err)
}

View File

@@ -8,12 +8,10 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"
"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/utils/logger"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"git.ma-al.com/goc_daniel/b2b/app/view"
"golang.org/x/oauth2"
@@ -79,20 +77,12 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
// Find or create user
user, err := s.findOrCreateGoogleUser(userInfo)
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
}
// Update last login
now := time.Now()
user.LastLoginAt = &now
user.Country = nil
s.db.Save(user)
// 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/repos/cartsRepo"
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"
)
@@ -35,15 +34,6 @@ func (s *CartsService) CreateNewCart(user_id uint, name string) (model.CustomerC
// create new cart for customer
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
}

View File

@@ -12,7 +12,6 @@ import (
"git.ma-al.com/goc_daniel/b2b/app/templ/emails"
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/logger"
"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
func (s *EmailService) SendEmail(to, subject, body string) error {
if !s.config.Enabled {
logger.Debug("email service is disabled",
"service", "EmailService.SendEmail",
"to", to,
"subject", subject,
)
return fmt.Errorf("email service is disabled")
}
@@ -75,12 +69,6 @@ func (s *EmailService) SendEmail(to, subject, body string) error {
// Send email
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 {
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)
}
@@ -132,12 +120,9 @@ func (s *EmailService) SendNewUserAdminNotification(userEmail, userName, baseURL
// SendNewOrderPlacedNotification sends an email to admin when new order is placed
func (s *EmailService) SendNewOrderPlacedNotification(userID uint) error {
if s.config.AdminEmail == "" {
logger.Warn("no admin email setup in the config",
"service", "EmailService.SendNewOrderPlacedNotification",
"user_id", userID,
)
return nil
return nil // No admin email configured
}
subject := "New Order Created"
body := s.newOrderPlacedTemplate(userID)

View File

@@ -10,7 +10,6 @@ import (
routesRepo "git.ma-al.com/goc_daniel/b2b/app/repos/routesRepo"
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/logger"
"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
}
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)
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
}
menus := make([]model.B2BMenu, len(items))
for i := range items {
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)
menuMap := make(map[int]*model.B2BTopMenu, len(items))
roots := make([]*model.B2BTopMenu, 0)
for i := range items {
menu := &items[i]
menu.Children = make([]*model.B2BMenu, 0)
menu.Children = make([]*model.B2BTopMenu, 0)
menuMap[menu.MenuID] = menu
}
@@ -261,7 +226,7 @@ func buildMenu(items []model.B2BMenu) []*model.B2BMenu {
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) {

View File

@@ -1,19 +1,15 @@
package orderService
import (
"fmt"
"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/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/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/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/find"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
@@ -22,36 +18,19 @@ import (
type OrderService struct {
ordersRepo ordersRepo.UIOrdersRepo
cartsRepo cartsRepo.UICartsRepo
productsRepo productsRepo.UIProductsRepo
addressesService *addressesService.AddressesService
emailService *emailService.EmailService
actionRegistry *orderStatusActions.ActionRegistry
}
func New() *OrderService {
return &OrderService{
ordersRepo: ordersRepo.New(),
cartsRepo: cartsRepo.New(),
productsRepo: productsRepo.New(),
addressesService: addressesService.New(),
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) {
if !user.HasPermission(perms.OrdersViewAll) {
// 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++ {
address_unparsed, err := s.addressesService.ValidateAddressJson(list.Items[i].AddressString, list.Items[i].CountryID)
// log such errors
if err != nil {
logger.Warn("failed to validate address",
"service", "orderService",
"order_id", list.Items[i].OrderID,
"error", err.Error(),
)
fmt.Printf("err: %v\n", err)
}
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
}
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)
if err != nil {
return err
@@ -106,10 +82,8 @@ func (s *OrderService) PlaceNewOrder(user_id uint, cart_id uint, name string, co
name = *cart.Name
}
base_price, tax_incl, tax_excl, err := s.getOrderTotalPrice(user_id, cart_id, country_id)
// 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 {
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
err = s.cartsRepo.RemoveCart(user_id, cart_id)
if err != nil {
logger.Warn("failed to remove cart after order placement",
"service", "orderService",
"user_id", user_id,
"cart_id", cart_id,
"error", err.Error(),
)
// Log error but don't fail placing order
_ = err
}
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 {
@@ -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)
}
func (s *OrderService) ChangeOrderStatus(userId, orderId uint, newStatus enums.OrderStatus) error {
order, err := s.ordersRepo.Get(orderId)
if err != nil {
return err
}
if order == nil {
return responseErrors.ErrOrderNotFound
}
if !ValidStatuses[newStatus] {
return responseErrors.ErrInvalidStatus
}
err = s.ordersRepo.ChangeOrderStatus(order.OrderID, newStatus, userId)
// This is obiously just an initial version of this function
func (s *OrderService) ChangeOrderStatus(user *model.Customer, order_id uint, status string) error {
if !user.HasPermission(perms.OrdersModifyAll) {
exists, err := s.ordersRepo.UserHasOrder(user.ID, order_id)
if err != nil {
return err
}
actionCtx := orderStatusActions.ActionContext{
Order: order,
UserId: &userId,
EmailService: s.emailService,
if !exists {
return responseErrors.ErrUserHasNoSuchOrder
}
}
go func() {
_ = 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
return s.ordersRepo.ChangeOrderStatus(order_id, status)
}

View File

@@ -14,14 +14,6 @@ func GetLangID(c fiber.Ctx) (uint, bool) {
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) {
user_locale, ok := c.Locals(constdata.USER_LOCALE).(*model.UserLocale)
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
ErrEmptyCart = errors.New("the cart is empty")
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
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, ErrUserHasNoSuchAddress),
errors.Is(err, ErrInvalidCountryID),
errors.Is(err, ErrInvalidAddressJSON),
errors.Is(err, ErrInvalidStatus):
errors.Is(err, ErrInvalidAddressJSON):
return fiber.StatusBadRequest
case errors.Is(err, ErrSpecificPriceNotFound):
return fiber.StatusNotFound

16
bo/components.d.ts vendored
View File

@@ -11,8 +11,6 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
'>': typeof import('./src/components/admin/product/ <TabGeneralSkeleton v-if="addProductStore.loadingCategories" />.vue')['default']
AddProduct: typeof import('./src/components/admin/AddProduct.vue')['default']
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
@@ -23,7 +21,6 @@ declare module 'vue' {
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
LayoutSkeleton: typeof import('./src/components/ui/LayoutSkeleton.vue')['default']
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
@@ -44,26 +41,15 @@ declare module 'vue' {
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.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']
RouterView: typeof import('vue-router')['RouterView']
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
TabGeneral: typeof import('./src/components/admin/product/TabGeneral.vue')['default']
TabGeneralSceleton: typeof import('./src/components/admin/product/TabGeneralSceleton.vue')['default']
TabGeneralSkeleton: typeof import('./src/components/admin/product/TabGeneralSkeleton.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']
TopBar: typeof import('./src/components/TopBar.vue')['default']
TopBarLogin: typeof import('./src/components/TopBarLogin.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']
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']
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']
@@ -84,10 +70,10 @@ declare module 'vue' {
UsersList: typeof import('./src/components/admin/UsersList.vue')['default']
UsersSearch: typeof import('./src/components/admin/UsersSearch.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']
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']
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']
}
}

6889
bo/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,6 @@
"dependencies": {
"@nuxt/ui": "^4.6.0",
"@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",
"pinia": "^3.0.4",
"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,8 +1,9 @@
<template>
<div class="flex flex-col md:flex-row gap-10">
<div class="w-full flex flex-col items-center gap-4">
<UTable :data="usersList" :columns="columns" class="flex-1 w-full"
:ui="{ root: 'max-w-100wv overflow-auto!' }" />
<UTable :data="usersList" :columns="columns" class="flex-1 w-full" :ui="{
root: 'max-w-100wv overflow-auto!'
}" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div>
</div>

View File

@@ -2,9 +2,11 @@
<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>
<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"
class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
class="flex-1 w-full" :ui="{
root: 'max-w-100wv overflow-auto!'
}" />
</div>
<p v-if="loading">Loading...</p>

View File

@@ -1,268 +0,0 @@
<template>
<TabGeneralSkeleton v-if="addProductStore.loadingCategories" />
<div v-else 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>
<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">
<legend class="px-1 block font-medium text-default text-[16px]">Powiązany produkt</legend>
<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.product_id"
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 bg-white">
<span>{{ p.name }}</span>
<button type="button" @click="removeRelated(p.product_id)"
class="text-red-500 transition-colors ml-2 cursor-pointer hover:text-red-700">
<UIcon name="i-lucide-x" class="text-base" />
</button>
</div>
</div>
<div v-if="relatedResults.length > 0"
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) bg-white dark:bg-neutral-900 max-h-80 overflow-auto">
<button v-for="p in relatedResults" :key="p.product_id" 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 max-h-80'
}">
<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'
import TabGeneralSkeleton from './TabGeneralSkeleton.vue'
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);
} else {
item.icon = 'i-lucide-file-text'
}
}
return menu;
}
//filter category - 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) {
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.product_id === product.product_id)) {
addProductStore.relatedProducts.push(product)
}
relatedSearch.value = ''
relatedResults.value = []
}
function removeRelated(id: number) {
const idx = addProductStore.relatedProducts.findIndex(p => p.product_id === 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
}
}
addProductStore.loadCategories()
</script>

View File

@@ -1,52 +0,0 @@
<template>
<div class="space-y-5 animate-pulse">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-1.5">
<div class="h-3.5 w-24 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
</div>
<div class="space-y-1.5">
<div class="h-3.5 w-28 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-1.5">
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
</div>
<div class="space-y-1.5">
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
</div>
</div>
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
<div class="h-4 w-16 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="flex gap-3">
<div v-for="i in 3" :key="i" class="w-36 h-36 rounded-xl bg-gray-200 dark:bg-neutral-700 shrink-0" />
</div>
</div>
<div class="space-y-2">
<div class="h-3.5 w-32 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
</div>
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
<div class="h-4 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
<div class="h-9 w-52 rounded-lg bg-gray-200 dark:bg-neutral-700" />
<div class="space-y-2">
<div v-for="i in 5" :key="i" class="flex items-center gap-2.5 px-1.5 py-1">
<div class="h-4 w-4 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700"
:class="['w-32', 'w-48', 'w-40', 'w-36', 'w-44'][i - 1]" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</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>
<div class="flex flex-col gap-5">
<div class="flex justify-between items-center">
<div class="">
<div class="flex flex-col gap-5 mb-6">
<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" />
{{ t('Add Address') }}
</UButton>
</div>
<div v-if="store.loading" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ t('Loading...') }}
</div>
<div v-else-if="store.error" class="text-center py-8 text-red-500">
{{ store.error }}
<div v-if="cartStore.addressLoading" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ t('Loading addresses...') }}
</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-for="addr in store.addresses" :key="addr.id"
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 v-else-if="cartStore.addressError" class="text-center py-8 text-red-500 dark:text-red-400">
{{ cartStore.addressError }}
</div>
<div class="flex flex-col items-end justify-between">
<UButton size="xs" color="error" variant="ghost" :title="t('Delete')"
@click="confirmDelete(addr.id)">
<div v-else-if="cartStore.paginatedAddresses.length"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<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]" />
</UButton>
<UButton size="sm" color="neutral" variant="outline" @click="openModal(addr)">
</button>
<UButton size="sm" color="neutral" variant="outline" @click="openEditModal(address)"
class="text-(--text-sky-light) dark:text-(--text-sky-dark) text-[13px]">
{{ t('edit') }}
<UIcon name="ic:sharp-edit" class="text-[14px]" />
<UIcon name="ic:sharp-edit" class="text-[15px]" />
</UButton>
</div>
</div>
</div>
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ t('No addresses found') }}
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">{{ t('No addresses found') }}</div>
<div class="mt-6 flex justify-center">
<UPagination v-model:page="page" :total="totalItems" :page-size="pageSize" />
</div>
<UModal v-model:open="showModal">
<template #header>
<h3 class="text-lg font-semibold text-black dark:text-white">
{{ editingId ? t('Edit Address') : t('Add Address') }}
</h3>
</template>
<template #body>
<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)">
<UModal v-model:open="showModal" :overlay="true" class="max-w-md mx-auto">
<template #content>
<div class="p-6 flex flex-col gap-6">
<p class="text-[20px] text-black dark:text-white ">Address</p>
<UForm @submit="saveAddress" class="space-y-4" :validate="validate" :state="formData">
<template v-for="field in formFieldKeys" :key="field">
<UFormField :label="fieldLabel(field)" :name="field"
:required="field !== 'address_line2'">
<UInput v-model="formData[field]" :placeholder="fieldLabel(field)" class="w-full" />
</UFormField>
</template>
<div class="flex justify-end gap-2 pt-2">
<UButton variant="outline" color="neutral" @click="showModal = false">
<div class="flex justify-end gap-2">
<UButton variant="outline" color="neutral" @click="closeModal">
{{ t('Cancel') }}
</UButton>
<UButton type="submit" color="info">
<UButton type="submit" color="info" class="cursor-pointer">
{{ t('Save') }}
</UButton>
</div>
@@ -91,25 +79,24 @@
</div>
</template>
</UModal>
<UModal v-model:open="showDeleteConfirm">
<template #body>
<div class="flex flex-col items-center gap-3 py-2">
<UIcon name="f7:exclamationmark-triangle" class="text-[40px] text-red-600" />
<p class="font-semibold text-black dark:text-white">{{ t('Confirm Delete') }}</p>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ t('Are you sure you want to delete this address?') }}
<UModal v-model:open="showDeleteConfirm" :overlay="true" class="max-w-md mx-auto">
<template #content>
<div class="p-6 flex flex-col gap-3">
<div class="flex flex-col gap-2 justify-center items-center">
<p class="flex items-end gap-2 dark:text-white text-black">
<UIcon name='f7:exclamationmark-triangle' class="text-[35px] text-red-700" />
Confirm Delete
</p>
<p class="text-gray-700 dark:text-gray-300">
{{ t('Are you sure you want to delete this address?') }}</p>
</div>
</template>
<template #footer>
<div class="flex justify-center gap-4">
<UButton variant="outline" color="neutral" @click="showDeleteConfirm = false">
{{ t('Cancel') }}
</UButton>
<UButton variant="outline" color="error" @click="deleteAddress">
{{ t('Delete') }}
<div class="flex justify-center gap-5">
<UButton variant="outline" color="neutral" @click="showDeleteConfirm = false"
class="dark:text-white text-black">{{ t('Cancel') }}
</UButton>
<UButton variant="outline" color="neutral" @click="deleteAddress" class="text-red-700">
{{ t('Delete') }}</UButton>
</div>
</div>
</template>
</UModal>
@@ -117,33 +104,53 @@
</template>
<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 { countries } from '@/router/langs'
import { useAddressStore } from '@/stores/customer/address'
import type { Country } from '@/types'
import type { Address } from '@/stores/customer/address'
import { currentCountry } from '@/router/langs'
type AddressFormState = Record<string, string>
const cartStore = useCartStore()
const { t } = useI18n()
const store = useAddressStore()
// --- Modal state ---
const searchQuery = ref(cartStore.addressSearchQuery)
const showModal = ref(false)
const editingId = ref<number | null>(null)
const selectedCountry = ref<Country | null>(null)
const templateLoading = ref(false)
const template = ref<Record<string, string>>({})
const formData = reactive<Record<string, string>>({})
const isEditing = ref(false)
const editingAddressId = ref<number | null>(null)
const addressTemplate = ref<AddressFormState | null>(null)
const formData = reactive<AddressFormState>({})
const templateKeys = computed(() => Object.keys(template.value))
const optionalFields = new Set(['address_line2'])
// --- Delete state ---
const showDeleteConfirm = ref(false)
const deleteId = ref<number | null>(null)
const addressToDelete = ref<number | null>(null)
const fieldLabels: Record<string, string> = {
recipient: 'Recipient',
const page = computed<number>({
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',
thoroughfare: 'Street',
building_no: 'Building No',
@@ -152,88 +159,90 @@ const fieldLabels: Record<string, string> = {
orientation_number: 'Orientation Number',
apartment_no: 'Apartment No',
sub_building: 'Sub Building',
postal_code: 'Zip Code',
post_town: 'City',
city: 'City',
county: 'County',
region: 'Region',
voivodeship: 'Region / Voivodeship',
address_line2: 'Address Line 2'
address_line2: 'Address Line 2',
recipient: 'Recipient'
}
function fieldLabel(key: string) {
return t(fieldLabels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()))
return t(labels[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (chr) => chr.toUpperCase()))
}
function validate() {
return templateKeys.value
.filter((key) => !optionalFields.has(key) && !formData[key]?.trim())
.map((key) => ({ name: key, message: t(`${fieldLabel(key)} is required`) }))
}
async function openCreateModal() {
resetForm()
isEditing.value = false
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
const template = await cartStore.getAddressTemplate(Number(currentCountry.value?.id) | 2).catch(() => null)
if (template) {
addressTemplate.value = template
clearFormData()
Object.assign(formData, template)
}
showModal.value = true
}
async function onCountryChange(country: Country | null) {
if (!country) {
template.value = {}
Object.keys(formData).forEach((k) => delete formData[k])
return
}
await loadTemplate(country.id)
async function openEditModal(address: any) {
currentCountry.value = address.country_id || 1
const template = await cartStore.getAddressTemplate(address.country_id | 2).catch(() => null)
if (template) {
addressTemplate.value = template
clearFormData()
Object.assign(formData, template)
}
async function loadTemplate(countryId: number, existing?: Record<string, string>) {
templateLoading.value = true
try {
const tpl = await store.getTemplate(countryId)
template.value = tpl
applyTemplate(tpl, existing)
} finally {
templateLoading.value = false
}
if (address.address_info) {
formFieldKeys.value.forEach((key) => {
formData[key] = address.address_info[key] ?? ''
})
}
async function save() {
if (!selectedCountry.value) return
if (editingId.value) {
await store.updateAddress(editingId.value, selectedCountry.value.id, { ...formData })
} else {
await store.createAddress(selectedCountry.value.id, { ...formData })
isEditing.value = true
editingAddressId.value = address.id
showModal.value = true
}
function resetForm() {
clearFormData()
editingAddressId.value = null
}
function closeModal() {
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) {
deleteId.value = id
addressToDelete.value = id
showDeleteConfirm.value = true
}
async function deleteAddress() {
if (deleteId.value) await store.deleteAddress(deleteId.value)
showDeleteConfirm.value = false
deleteId.value = null
if (addressToDelete.value) {
await cartStore.deleteAddress(addressToDelete.value)
}
showDeleteConfirm.value = false
addressToDelete.value = null
}
store.fetchAddresses()
</script>

View File

@@ -1,10 +1,15 @@
<template>
<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 items-start gap-2">
<h1 class="text-2xl font-bold text-black dark:text-white">{{ t('Shopping Carts') }}</h1>
<div class="flex gap-3">
<UButton color="primary" @click="showCreateModal = true" :disabled="cartStore.carts?.length >= 10"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
<UTooltip :delay-duration="0" text="You can create up to 10 carts.">
<UIcon name="icon-park-outline:attention" class="text-[15px] text-blue-500" />
</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" />
{{ t('New Cart') }}
</UButton>
@@ -44,25 +49,23 @@
</div>
<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>
<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">
<UInput v-model="newCartName" :placeholder="t('Cart name')"
class="w-full bg-white dark:bg-gray-800 text-black dark:text-white" />
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<UButton variant="outline" color="neutral" @click="showCreateModal = false">
{{ t('Cancel') }}
</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">
{{ t('Create') }}
</UButton>
</div>
</div>
</template>
</UModal>
</template>

View File

@@ -1,19 +1,11 @@
<template>
<suspense>
<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>
<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>
</div>
<div v-else-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
</div> -->
<div v-if="customerProductStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ customerProductStore.error }}
</div>
<div v-else class="overflow-x-auto">
@@ -21,8 +13,7 @@
<CategoryMenu />
<UTable :data="customerProductStore.productsList" :columns="columns" class="flex-1">
<template #expanded="{ row }">
<UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild"
:ui="{
<UTable :data="customerProductStore.productsList.slice(0, 3)" :columns="columnsChild" :ui="{
thead: 'hidden'
}" />
</template>
@@ -42,95 +33,28 @@
</template>
<script setup lang="ts">
import { ref, watch, h, resolveComponent, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ref, watch, h, resolveComponent } from 'vue'
import { useRouter } from 'vue-router'
import type { TableColumn } from '@nuxt/ui'
import CategoryMenu from '../inner/CategoryMenu.vue'
import { useCustomerProductStore } from '@/stores/customer/customer-product'
import type { Product } from '@/stores/customer/customer-product'
import { useCartStore } from '@/stores/customer/cart'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useTableState } from '@/composable/useTableState'
import { debounce } from '@/composable/useDebouncedSearch'
const customerProductStore = useCustomerProductStore()
const router = useRouter()
const route = useRoute()
const { page, sortField, filters } = useTableState()
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
}
})
}
})
function debounce(fn: Function, delay = 400) {
let t: any
return (...args: any[]) => {
clearTimeout(t)
t = setTimeout(() => fn(...args), delay)
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) => {
@@ -142,29 +66,11 @@ const updateFilter = debounce((columnId: string, val: string) => {
filters.value = newFilters
}, 400)
function goToProduct(productId: number) {
router.push({
name: 'customer-product-details',
params: { product_id: productId }
})
}
const selectedCount = ref({
product_id: null,
product_id: null as number | null,
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 UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton')
@@ -215,7 +121,6 @@ const columns: TableColumn<Product>[] = [
})
])
},
// header: '#',
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 toast = useToast()
async function addToCart(product_id: number) {
@@ -465,11 +379,17 @@ async function addToCart(product_id: number) {
duration: 5000
})
}
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>

View File

@@ -4,7 +4,11 @@
Search Products
</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..."
v-model="searchQuery" class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
</div>
@@ -13,9 +17,12 @@
Loading...
</div>
<div v-else-if="products.length" class="mt-6 w-full">
<UTable :data="products" :columns="columns" class="flex-1 w-full"
:ui="{ root: 'max-w-100wv overflow-auto!' }" />
<p v-else-if="error" class="mt-6 text-red-500">
{{ error }}
</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>
<p v-else-if="searchQuery" class="mt-6">
@@ -25,130 +32,333 @@
</template>
<script setup lang="ts">
import { useFetchJson } from '@/composable/useFetchJson'
import { ref, watch, computed, resolveComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { debounce } from 'chart.js/helpers'
import { h } from 'vue'
import { ref, watch } from 'vue'
import { h, resolveComponent } from 'vue'
import type { TableColumn } from '@nuxt/ui'
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 products = ref<Product[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const {
searchQuery,
products,
loading,
error,
searchByReference,
selectedCount,
fetchProducts,
addToCart,
goToProduct
} = useProductSearchApi()
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 { sortField, filters } = useTableState()
const debouncedFetch = debounce(fetchProducts, 400)
watch(searchQuery, () => {
watch([searchQuery, searchByReference], () => {
debouncedFetch()
})
const route = useRoute()
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 UInputNumber = resolveComponent('UInputNumber')
const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton')
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>[] = [
{
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',
header: 'ID',
cell: ({ row }) => `#${row.getValue('product_id')}`
header: ({ column }) => {
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',
header: 'Image',
cell: ({ row }) =>
h('img', {
src: row.getValue('image_link'),
style: 'width:40px;height:40px;object-fit:cover;',
onError: (e: Event) => {
(e.target as HTMLImageElement).src = errorImg
}
cell: ({ row }) => {
return h('img', {
src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;'
})
}
},
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.getValue('name')
header: ({ column }) => {
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',
header: 'In stock',
cell: ({ row }) => row.getValue('quantity')
header: ({ column }) => {
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',
header: '',
cell: ({ row }) =>
h(
UButton,
{
onClick: () =>
router.push({
name: 'admin-product-details',
params: {
accessorKey: 'count',
header: 'Count',
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,
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',
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>
<style scoped>

View File

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

View File

@@ -1,6 +1,6 @@
<template>
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
:ui="{ base: 'p-8', root: 'p-2' }"
:ui="{ base: 'p-8 sm:px-16', root: 'p-2' }"
class="min-w-full border rounded-md bg-white! border-(--border-light)" placeholder="Write there ...">
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8 flex-wrap!">
<template #link>

View File

@@ -1,52 +0,0 @@
<template>
<div class="flex flex-1 overflow-x-hidden h-svh animate-pulse">
<!-- Sidebar -->
<div class="flex flex-col shrink-0 w-52 bg-elevated/25 border-r border-default h-full">
<!-- Sidebar header -->
<div class="flex items-center gap-2 p-3 border-b border-default h-(--ui-header-height)">
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
<div class="h-4 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
</div>
<!-- Sidebar nav items -->
<div class="flex flex-col gap-1 p-2 flex-1">
<div v-for="i in 7" :key="i" class="flex items-center gap-2.5 px-1.5 py-1.5">
<div class="h-5 w-5 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700" :class="['w-20','w-28','w-24','w-16','w-28','w-20','w-24'][i-1]" />
</div>
</div>
<!-- Sidebar footer -->
<div class="p-3 border-t border-default">
<div class="flex items-center gap-2">
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
<div class="h-3.5 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
</div>
</div>
</div>
<!-- Main area -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Header -->
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
<!-- Left: toggle + title -->
<div class="flex items-center gap-4">
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
<div class="h-5 w-36 rounded bg-gray-200 dark:bg-neutral-700" />
</div>
<!-- Right: controls -->
<div class="hidden md:flex items-center gap-3">
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
<div class="h-8 w-24 rounded-lg bg-gray-200 dark:bg-neutral-700" />
</div>
</div>
<!-- Page content placeholder -->
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
<slot />
</div>
</div>
</div>
</template>

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

@@ -1,7 +1,5 @@
<template>
<LayoutSkeleton v-if="loadingLayout" />
<div v-else class="flex flex-1 overflow-x-hidden h-svh">
<div class="flex flex-1 overflow-x-hidden h-svh">
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
container: 'h-full z-80',
inner: 'bg-elevated/25 divide-transparent',
@@ -90,7 +88,6 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import LayoutSkeleton from '@/components/ui/LayoutSkeleton.vue'
import { useColorMode } from '@vueuse/core'
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
@@ -101,9 +98,7 @@ const userStore = useUserStore()
const route = useRoute()
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
const loadingLayout = ref(true)
userStore.getUser().finally(() => { loadingLayout.value = false })
await userStore.getUser()
const open = ref(true)
const colorMode = useColorMode()
@@ -210,7 +205,8 @@ const menu = ref<TopMenuItem[] | null>(null)
async function getTopMenu() {
try {
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
menu.value = items[0]?.children || []
menu.value = items
} catch (err) {
console.log(err)
}

View File

@@ -182,7 +182,7 @@ const menu = ref<TopMenuItem[] | null>(null)
const Id = Number(route.params.user_id)
async function cmGetTopMenu() {
try {
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-customer-management-menu`)
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
menu.value = items
} catch (err) {

View File

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

View File

@@ -12,7 +12,10 @@ export const currentCountry = ref<Country>()
const defLang = ref<Language>()
const defCountry = ref<Country>()
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() {
try {
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() {
try {
const { items } = await useFetchJson<Country[]>('/api/v1/restricted/langs-and-countries/get-countries')
@@ -38,10 +43,13 @@ export async function initCountryCurrency() {
let idfromcookie = null
const cc = cookie.getCookie('country_id')
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)
currentCountry.value = idfromcookie ?? defCountry.value
console.log(defCountry.value);
console.log(currentCountry.value);
} catch (error) {
console.error('Failed to fetch languages:', error)
}
@@ -52,6 +60,7 @@ export async function switchLocalization() {
await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}

View File

@@ -1,168 +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[]>([])
const loadingCategories = ref(true)
async function loadCategories() {
loadingCategories.value = true
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
loadingCategories.value = false
}
return {
loading,
saving,
error,
successMessage,
form,
images,
selectedCategories,
relatedProducts,
variants,
variantSaving,
variantErrors,
categories,
loadingCategories,
loadProduct,
addImageFiles,
removeImage,
setCover,
resetForm,
loadCategories
}
})

View File

@@ -1,58 +1,231 @@
import { useFetchJson } from '@/composable/useFetchJson'
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 {
id: number
country_id: number
address_unparsed: Record<string, string>
street: string
zipCode: string
city: string
country: string
}
export const useAddressStore = defineStore('address', () => {
const addresses = ref<Address[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchAddresses() {
loading.value = true
error.value = null
try {
const res = await useFetchJson<Address[]>('/api/v1/restricted/addresses/retrieve-addresses')
addresses.value = res.items ?? []
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Failed to load addresses'
} finally {
loading.value = false
}
const currentPage = ref(1)
const pageSize = 20
const searchQuery = ref('')
function initMockData() {
addresses.value = [
{ id: 1, street: 'Main Street 123', zipCode: '10-001', city: 'New York', country: 'United States' },
{ id: 2, street: 'Oak Avenue 123', zipCode: '90-001', city: 'Los Angeles', country: 'United States' },
{ id: 3, street: 'Pine Road 123', zipCode: '60-601', city: 'Chicago', country: 'United States' }
]
}
async function deleteAddress(id: number) {
await useFetchJson(`/api/v1/restricted/addresses/delete-address?address_id=${id}`, { method: 'DELETE' })
addresses.value = addresses.value.filter((a) => a.id !== id)
}
const filteredAddresses = computed(() => {
if (!searchQuery.value) return addresses.value
async function getTemplate(countryId: number): Promise<Record<string, string>> {
const res = await useFetchJson<Record<string, string>>(
`/api/v1/restricted/addresses/get-template?country_id=${countryId}`
const query = searchQuery.value.toLowerCase()
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 ?? {}
})
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 createAddress(countryId: number, data: Record<string, string>) {
await useFetchJson(`/api/v1/restricted/addresses/add-new-address?country_id=${countryId}`, {
function normalize(data: AddressFormData): AddressFormData {
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',
body: JSON.stringify(data)
body: JSON.stringify(formData)
})
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
}
}
async function updateAddress(id: number, countryId: number, data: Record<string, string>) {
await useFetchJson(`/api/v1/restricted/addresses/modify-address?country_id=${countryId}&address_id=${id}`, {
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(data)
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
}
}
return { addresses, loading, error, fetchAddresses, deleteAddress, getTemplate, createAddress, updateAddress }
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 {
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)

View File

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

View File

@@ -11,8 +11,12 @@ export const useUserStore = defineStore('user', () => {
error.value = null
try {
const data = await useFetchJson<User>(`/api/v1/restricted/customer`)
console.log('getUser API response:', data)
const response: User = (data as any).items ?? data
console.log('User response:', response)
user.value = response
return response
} catch (err: any) {
error.value = err?.message ?? 'Unknown error'

View File

@@ -18,93 +18,3 @@ export interface Product {
image_link: string
link_rewrite: string
}
export interface ProductCategory {
id_category: number
name: string
}
export interface ProductRelated {
product_id: 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:
name: Delete Index - MeiliSearch
type: http
seq: 8
seq: 7
http:
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:
name: currency
type: folder
seq: 10
seq: 9
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
info:
name: customer
type: folder
seq: 11
seq: 10
request:
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:
name: product
type: folder
seq: 9
seq: 8
request:
auth: inherit

View File

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

View File

@@ -5,14 +5,15 @@ info:
http:
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:
- name: lang_id
value: "1"
value: "0"
type: query
- name: country_id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true

View File

@@ -5,7 +5,7 @@ info:
http:
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:
- name: cart_id
value: "1"
@@ -17,7 +17,7 @@ http:
value: "1115"
type: query
- name: amount
value: "2"
value: "1"
type: query
- name: set_amount
value: "true"

View File

@@ -56,8 +56,7 @@ INSERT IGNORE INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `act
', 1),
(17, 'customer-management-profile', ':user_id/profile', '/components/customer-management/Profile.vue', '{
"guest":true,
"name": "Profile",
"layout": "management"
"name": "Profile"
}
', 1),
(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),
(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),
(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);
(24, 'customer-search-products', 'search-products', '/components/customer/PageSearchProducts.vue', '{ "guest":true, "name": "Carts" }', 1);
CREATE TABLE IF NOT EXISTS b2b_top_menu (
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": ""
}
}
}', 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);
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
DROP TABLE IF EXISTS b2b_routes;
DROP TABLE IF EXISTS b2b_top_menu;
DROP TABLE IF EXISTS b2b_customer_management_menu;
DROP FUNCTION IF EXISTS `slugify_eu`;

View File

@@ -113,7 +113,7 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
created_at DATETIME(6) NULL,
updated_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;
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,
address_string TEXT 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_countries FOREIGN KEY (country_id) REFERENCES b2b_countries(id) ON DELETE NO ACTION ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -462,30 +457,6 @@ END$$
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
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 ('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 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
@@ -129,15 +108,12 @@ INSERT INTO `b2b_route_roles` (`route_id`, `role_id`) VALUES
(15, '3'),
(16, '2'),
(16, '3'),
(17, '2'),
(17, '3'),
(17, '1'),
(18, '2'),
(18, '3'),
(19, '1'),
(20, '1'),
(21, '1'),
(22, '1'),
(24, '1'),
(25, '2'),
(25, '3');
(24, '1');
-- +goose Down

View File

@@ -415,7 +415,7 @@ BEGIN
LEFT JOIN ps_manufacturer m
ON m.id_manufacturer = p.id_manufacturer
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
LIMIT 1;