refactor: restructure in entirety
This commit is contained in:
108
internal/handler/account.go
Normal file
108
internal/handler/account.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"billit/internal/logic"
|
||||
"billit/internal/view"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// AccountHandlers holds references for account operations
|
||||
type AccountHandlers struct {
|
||||
db database.Service
|
||||
auth *logic.AuthService
|
||||
}
|
||||
|
||||
// NewAccountHandlers creates handlers with db and auth access
|
||||
func NewAccountHandlers(db database.Service, authService *logic.AuthService) *AccountHandlers {
|
||||
return &AccountHandlers{db: db, auth: authService}
|
||||
}
|
||||
|
||||
// AccountPageHandler renders the /account page
|
||||
func (h *AccountHandlers) AccountPageHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
user, err := h.db.GetUserByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return view.RenderServerError(c, "Failed to load account details.")
|
||||
}
|
||||
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", ""))
|
||||
}
|
||||
|
||||
// UpdateDetailsHandler handles POST /account/details
|
||||
func (h *AccountHandlers) UpdateDetailsHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
user, err := h.db.GetUserByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return view.RenderServerError(c, "Failed to load account details.")
|
||||
}
|
||||
|
||||
companyDetails := c.FormValue("company_details")
|
||||
bankDetails := c.FormValue("bank_details")
|
||||
invoicePrefix := c.FormValue("invoice_prefix")
|
||||
if invoicePrefix == "" {
|
||||
invoicePrefix = "INV" // Default prefix
|
||||
}
|
||||
|
||||
err = h.db.UpdateUserDetails(userID, companyDetails, bankDetails, invoicePrefix)
|
||||
if err != nil {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "Failed to update details"))
|
||||
}
|
||||
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, companyDetails, bankDetails, invoicePrefix, "Details updated successfully", ""))
|
||||
}
|
||||
|
||||
// ChangePasswordHandler handles POST /account/password
|
||||
func (h *AccountHandlers) ChangePasswordHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
user, err := h.db.GetUserByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return view.RenderServerError(c, "Failed to load account details.")
|
||||
}
|
||||
|
||||
currentPassword := c.FormValue("current_password")
|
||||
newPassword := c.FormValue("new_password")
|
||||
confirmPassword := c.FormValue("confirm_password")
|
||||
|
||||
// Validate current password
|
||||
if !logic.CheckPassword(currentPassword, user.Password) {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "Current password is incorrect"))
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
if len(newPassword) < 8 {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "New password must be at least 8 characters"))
|
||||
}
|
||||
|
||||
if newPassword != confirmPassword {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "New passwords do not match"))
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hash, err := logic.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "Failed to update password"))
|
||||
}
|
||||
|
||||
// Update password in database
|
||||
err = h.db.UpdateUserPassword(userID, hash)
|
||||
if err != nil {
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "", "Failed to update password"))
|
||||
}
|
||||
|
||||
return view.Render(c, view.AccountPage(user.Email, user.CreatedAt, user.CompanyDetails, user.BankDetails, user.InvoicePrefix, "Password changed successfully", ""))
|
||||
}
|
||||
176
internal/handler/auth.go
Normal file
176
internal/handler/auth.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"billit/internal/logic"
|
||||
"billit/internal/view"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// AuthHandlers holds auth service reference
|
||||
type AuthHandlers struct {
|
||||
auth *logic.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandlers creates handlers with auth service
|
||||
func NewAuthHandlers(authService *logic.AuthService) *AuthHandlers {
|
||||
return &AuthHandlers{auth: authService}
|
||||
}
|
||||
|
||||
// createAuthCookie creates an HTTP-only secure cookie for the token
|
||||
func createAuthCookie(token string) *http.Cookie {
|
||||
domain := os.Getenv("COOKIE_DOMAIN")
|
||||
secure := os.Getenv("COOKIE_SECURE") == "true"
|
||||
|
||||
return &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
MaxAge: int(24 * time.Hour.Seconds()), // Match token duration
|
||||
HttpOnly: true, // Prevents JavaScript access
|
||||
Secure: secure, // Only send over HTTPS in production
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
}
|
||||
|
||||
// clearAuthCookie returns a cookie that clears the auth token
|
||||
func clearAuthCookie() *http.Cookie {
|
||||
domain := os.Getenv("COOKIE_DOMAIN")
|
||||
secure := os.Getenv("COOKIE_SECURE") == "true"
|
||||
|
||||
return &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Domain: domain,
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// LoginPageHandler renders the login page (home page)
|
||||
func (h *AuthHandlers) LoginPageHandler(c echo.Context) error {
|
||||
// Check if already logged in
|
||||
cookie, err := c.Cookie("auth_token")
|
||||
if err == nil && cookie.Value != "" {
|
||||
_, err := h.auth.ValidateToken(cookie.Value)
|
||||
if err == nil {
|
||||
// Already logged in, redirect to home
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
}
|
||||
// Capture redirect URL from query param
|
||||
redirectURL := c.QueryParam("redirect")
|
||||
return view.Render(c, view.LoginPage("", "", redirectURL))
|
||||
}
|
||||
|
||||
// LoginHandler handles login form submission
|
||||
func (h *AuthHandlers) LoginHandler(c echo.Context) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
password := c.FormValue("password")
|
||||
redirectURL := c.FormValue("redirect")
|
||||
|
||||
if email == "" || password == "" {
|
||||
return view.Render(c, view.LoginPage("Email and password are required", email, redirectURL))
|
||||
}
|
||||
|
||||
token, err := h.auth.Login(email, password)
|
||||
if err != nil {
|
||||
return view.Render(c, view.LoginPage("Invalid email or password", email, redirectURL))
|
||||
}
|
||||
|
||||
// Set HTTP-only cookie
|
||||
cookie := createAuthCookie(token)
|
||||
c.SetCookie(cookie)
|
||||
|
||||
// Redirect to original URL or home page
|
||||
if redirectURL != "" && strings.HasPrefix(redirectURL, "/") {
|
||||
return c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
|
||||
// RegisterPageHandler renders the registration page
|
||||
func (h *AuthHandlers) RegisterPageHandler(c echo.Context) error {
|
||||
return view.Render(c, view.RegisterPage("", ""))
|
||||
}
|
||||
|
||||
// RegisterHandler handles registration form submission
|
||||
func (h *AuthHandlers) RegisterHandler(c echo.Context) error {
|
||||
email := strings.TrimSpace(c.FormValue("email"))
|
||||
password := c.FormValue("password")
|
||||
confirmPassword := c.FormValue("confirm_password")
|
||||
|
||||
if email == "" || password == "" {
|
||||
return view.Render(c, view.RegisterPage("Email and password are required", email))
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
return view.Render(c, view.RegisterPage("Passwords do not match", email))
|
||||
}
|
||||
|
||||
if len(password) < 8 {
|
||||
return view.Render(c, view.RegisterPage("Password must be at least 8 characters", email))
|
||||
}
|
||||
|
||||
_, err := h.auth.Register(email, password)
|
||||
if err != nil {
|
||||
if err == logic.ErrUserExists {
|
||||
return view.Render(c, view.RegisterPage("An account with this email already exists", email))
|
||||
}
|
||||
return view.Render(c, view.RegisterPage(err.Error(), email))
|
||||
}
|
||||
|
||||
// Auto-login after registration
|
||||
token, err := h.auth.Login(email, password)
|
||||
if err != nil {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
cookie := createAuthCookie(token)
|
||||
c.SetCookie(cookie)
|
||||
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
|
||||
// LogoutHandler clears the auth cookie and redirects to login
|
||||
func (h *AuthHandlers) LogoutHandler(c echo.Context) error {
|
||||
cookie := clearAuthCookie()
|
||||
c.SetCookie(cookie)
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
// AuthMiddleware protects routes that require authentication
|
||||
func (h *AuthHandlers) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
cookie, err := c.Cookie("auth_token")
|
||||
if err != nil || cookie.Value == "" {
|
||||
// No cookie - redirect to login with original URL for post-login redirect
|
||||
redirectPath := url.QueryEscape(c.Request().URL.RequestURI())
|
||||
return c.Redirect(http.StatusFound, "/?redirect="+redirectPath)
|
||||
}
|
||||
|
||||
claims, err := h.auth.ValidateToken(cookie.Value)
|
||||
if err != nil {
|
||||
// Invalid/expired token - show session expired dialog
|
||||
c.SetCookie(clearAuthCookie())
|
||||
redirectPath := url.QueryEscape(c.Request().URL.RequestURI())
|
||||
return view.Render(c, view.SessionExpiredPage(redirectPath))
|
||||
}
|
||||
|
||||
// Store user info in context
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("user_email", claims.Email)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
436
internal/handler/billing.go
Normal file
436
internal/handler/billing.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package handler
|
||||
|
||||
import "billit/internal/models"
|
||||
import "billit/internal/view"
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"billit/internal/logic"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// numberToWords converts a number to Indian English words (supports up to crores)
|
||||
func numberToWords(n float64) string {
|
||||
if n == 0 {
|
||||
return "Zero"
|
||||
}
|
||||
|
||||
ones := []string{"", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine",
|
||||
"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}
|
||||
tens := []string{"", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"}
|
||||
|
||||
convertLessThanHundred := func(n int) string {
|
||||
if n < 20 {
|
||||
return ones[n]
|
||||
}
|
||||
if n%10 == 0 {
|
||||
return tens[n/10]
|
||||
}
|
||||
return tens[n/10] + " " + ones[n%10]
|
||||
}
|
||||
|
||||
convertLessThanThousand := func(n int) string {
|
||||
if n < 100 {
|
||||
return convertLessThanHundred(n)
|
||||
}
|
||||
if n%100 == 0 {
|
||||
return ones[n/100] + " Hundred"
|
||||
}
|
||||
return ones[n/100] + " Hundred " + convertLessThanHundred(n%100)
|
||||
}
|
||||
// Split into rupees and paise
|
||||
rupees := int(math.Floor(n))
|
||||
paise := int(math.Round((n - float64(rupees)) * 100))
|
||||
|
||||
var result string
|
||||
|
||||
if rupees >= 10000000 { // Crores
|
||||
crores := rupees / 10000000
|
||||
rupees = rupees % 10000000
|
||||
result += convertLessThanThousand(crores) + " Crore "
|
||||
}
|
||||
if rupees >= 100000 { // Lakhs
|
||||
lakhs := rupees / 100000
|
||||
rupees = rupees % 100000
|
||||
result += convertLessThanHundred(lakhs) + " Lakh "
|
||||
}
|
||||
if rupees >= 1000 { // Thousands
|
||||
thousands := rupees / 1000
|
||||
rupees = rupees % 1000
|
||||
result += convertLessThanHundred(thousands) + " Thousand "
|
||||
}
|
||||
if rupees > 0 {
|
||||
result += convertLessThanThousand(rupees)
|
||||
}
|
||||
|
||||
result = strings.TrimSpace(result)
|
||||
if result == "" {
|
||||
result = "Zero"
|
||||
}
|
||||
result += " Rupees"
|
||||
|
||||
if paise > 0 {
|
||||
result += " and " + convertLessThanHundred(paise) + " Paise"
|
||||
}
|
||||
|
||||
return result + " Only"
|
||||
}
|
||||
|
||||
// BillingHandlers holds db reference for billing operations
|
||||
type BillingHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewBillingHandlers creates handlers with db access
|
||||
func NewBillingHandlers(db database.Service) *BillingHandlers {
|
||||
return &BillingHandlers{db: db}
|
||||
}
|
||||
|
||||
// BillingPageHandler renders the /billing page for creating bills
|
||||
func (h *BillingHandlers) BillingPageHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
products, err := h.db.GetAllProducts(userID)
|
||||
if err != nil {
|
||||
products = []models.Product{}
|
||||
}
|
||||
buyers, err := h.db.GetAllBuyerDetails(userID)
|
||||
if err != nil {
|
||||
buyers = []models.BuyerDetails{}
|
||||
}
|
||||
return view.Render(c, view.BillingPage(products, buyers))
|
||||
}
|
||||
|
||||
// CalculateBillHandler calculates the bill (HTMX endpoint)
|
||||
func (h *BillingHandlers) CalculateBillHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
customerType := c.FormValue("customer_type")
|
||||
regionType := c.FormValue("region_type")
|
||||
includeConvenienceFee := c.FormValue("include_convenience_fee") == "yes"
|
||||
|
||||
cType := logic.CustomerRetail
|
||||
if customerType == "wholesale" {
|
||||
cType = logic.CustomerWholesale
|
||||
}
|
||||
isInterState := regionType == "inter"
|
||||
|
||||
calculator := logic.NewCalculator()
|
||||
var items []logic.LineItem
|
||||
var totalFee float64
|
||||
|
||||
// Support up to 50 product slots for dynamic adding
|
||||
for i := 0; i < 50; i++ {
|
||||
sku := c.FormValue("product_sku_" + strconv.Itoa(i))
|
||||
qtyStr := c.FormValue("qty_" + strconv.Itoa(i))
|
||||
|
||||
if sku == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
qty, err := strconv.Atoi(qtyStr)
|
||||
if err != nil || qty <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get product from DB (user-scoped)
|
||||
dbProduct, err := h.db.GetProductBySKU(sku, userID)
|
||||
if err != nil || dbProduct == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to logic.Product
|
||||
product := logic.Product{
|
||||
SKU: dbProduct.SKU,
|
||||
Name: dbProduct.Name,
|
||||
HSNCode: dbProduct.HSNCode,
|
||||
BasePrice: dbProduct.BasePrice,
|
||||
WholesalePrice: dbProduct.WholesalePrice,
|
||||
GSTRate: logic.Rate(dbProduct.GSTRate),
|
||||
SmallOrderQty: dbProduct.SmallOrderQty,
|
||||
SmallOrderFee: dbProduct.SmallOrderFee,
|
||||
Unit: dbProduct.Unit,
|
||||
}
|
||||
|
||||
lineItem := calculator.CalculateLineItem(product, qty, cType, isInterState)
|
||||
items = append(items, lineItem)
|
||||
|
||||
// Apply per-product convenience fee if quantity is below threshold and checkbox is checked
|
||||
if includeConvenienceFee && product.SmallOrderQty > 0 && qty < product.SmallOrderQty && product.SmallOrderFee > 0 {
|
||||
totalFee += product.SmallOrderFee
|
||||
}
|
||||
}
|
||||
|
||||
invoice := calculator.CalculateInvoice(items, totalFee, isInterState)
|
||||
invoice.CustomerType = cType
|
||||
|
||||
return view.Render(c, view.InvoiceSummary(invoice))
|
||||
}
|
||||
|
||||
// GenerateBillHandler generates final invoice with UUID and persists to DB
|
||||
func (h *BillingHandlers) GenerateBillHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
customerType := c.FormValue("customer_type")
|
||||
regionType := c.FormValue("region_type")
|
||||
buyerID := c.FormValue("buyer_id")
|
||||
includeConvenienceFee := c.FormValue("include_convenience_fee") == "yes"
|
||||
|
||||
cType := logic.CustomerRetail
|
||||
if customerType == "wholesale" {
|
||||
cType = logic.CustomerWholesale
|
||||
}
|
||||
isInterState := regionType == "inter"
|
||||
|
||||
calculator := logic.NewCalculator()
|
||||
var items []logic.LineItem
|
||||
var totalFee float64
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
sku := c.FormValue("product_sku_" + strconv.Itoa(i))
|
||||
qtyStr := c.FormValue("qty_" + strconv.Itoa(i))
|
||||
|
||||
if sku == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
qty, err := strconv.Atoi(qtyStr)
|
||||
if err != nil || qty <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dbProduct, err := h.db.GetProductBySKU(sku, userID)
|
||||
if err != nil || dbProduct == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
product := logic.Product{
|
||||
SKU: dbProduct.SKU,
|
||||
Name: dbProduct.Name,
|
||||
HSNCode: dbProduct.HSNCode,
|
||||
BasePrice: dbProduct.BasePrice,
|
||||
WholesalePrice: dbProduct.WholesalePrice,
|
||||
GSTRate: logic.Rate(dbProduct.GSTRate),
|
||||
SmallOrderQty: dbProduct.SmallOrderQty,
|
||||
SmallOrderFee: dbProduct.SmallOrderFee,
|
||||
Unit: dbProduct.Unit,
|
||||
}
|
||||
|
||||
lineItem := calculator.CalculateLineItem(product, qty, cType, isInterState)
|
||||
items = append(items, lineItem)
|
||||
|
||||
// Apply per-product convenience fee if checkbox is checked
|
||||
if includeConvenienceFee && product.SmallOrderQty > 0 && qty < product.SmallOrderQty && product.SmallOrderFee > 0 {
|
||||
totalFee += product.SmallOrderFee
|
||||
}
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return c.String(http.StatusBadRequest, "No products selected")
|
||||
}
|
||||
|
||||
invoice := calculator.CalculateInvoice(items, totalFee, isInterState)
|
||||
invoice.CustomerType = cType
|
||||
|
||||
// Get user's company and bank details
|
||||
user, err := h.db.GetUserByID(userID)
|
||||
if err == nil && user != nil {
|
||||
invoice.CompanyDetails = user.CompanyDetails
|
||||
invoice.BankDetails = user.BankDetails
|
||||
}
|
||||
|
||||
// Get buyer details if selected
|
||||
if buyerID != "" {
|
||||
buyer, err := h.db.GetBuyerDetails(buyerID, userID)
|
||||
if err == nil && buyer != nil {
|
||||
invoice.BuyerName = buyer.Name
|
||||
invoice.BuyerDetails = buyer.Details
|
||||
}
|
||||
}
|
||||
|
||||
// Generate UUID for invoice
|
||||
invoiceID := uuid.New().String()
|
||||
|
||||
// Generate human-readable invoice ID
|
||||
humanReadableID, err := h.db.GetNextInvoiceNumber(userID)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to generate invoice number")
|
||||
}
|
||||
|
||||
// Persist to DB (user-scoped)
|
||||
if err := h.db.CreateInvoice(invoiceID, humanReadableID, invoice, userID); err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to save invoice")
|
||||
}
|
||||
|
||||
// Redirect to invoice view
|
||||
c.Response().Header().Set("HX-Redirect", fmt.Sprintf("/invoice/%s", invoiceID))
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
// ShowInvoiceHandler displays the invoice by UUID (requires auth)
|
||||
func (h *BillingHandlers) ShowInvoiceHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
invoiceID := c.Param("id")
|
||||
|
||||
inv, err := h.db.GetInvoice(invoiceID, userID)
|
||||
if err != nil || inv == nil {
|
||||
return view.RenderNotFound(c, "Invoice not found or you don't have access to it.")
|
||||
}
|
||||
|
||||
// Parse the JSON data back into Invoice struct
|
||||
var invoice logic.Invoice
|
||||
if err := json.Unmarshal([]byte(inv.Data), &invoice); err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to parse invoice data")
|
||||
}
|
||||
|
||||
// Generate QR code for invoice URL
|
||||
invoiceURL := fmt.Sprintf("%s://%s/invoice/%s", c.Scheme(), c.Request().Host, invoiceID)
|
||||
qrPNG, err := qrcode.Encode(invoiceURL, qrcode.Medium, 100)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to generate QR code")
|
||||
}
|
||||
qrBase64 := base64.StdEncoding.EncodeToString(qrPNG)
|
||||
|
||||
// Render printable invoice page with multi-page support
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTML)
|
||||
w := c.Response().Writer
|
||||
fmt.Fprint(w, "<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Invoice ")
|
||||
fmt.Fprintf(w, "%s</title>", invoiceID[:8])
|
||||
fmt.Fprint(w, "<link href='/assets/css/output.css' rel='stylesheet'>")
|
||||
fmt.Fprint(w, `<style type="text/css">
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-before: always; }
|
||||
@page { margin: 1cm; size: A4; }
|
||||
body { background: white !important; }
|
||||
.invoice-table th { background: #e0e0e0 !important; color: black !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.invoice-totals .row-total { background: #e0e0e0 !important; color: black !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
}
|
||||
.invoice-details-block { white-space: pre-wrap; font-size: 0.875rem; line-height: 1.4; }
|
||||
.invoice-footer-section { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; }
|
||||
</style>`)
|
||||
fmt.Fprint(w, "</head><body>")
|
||||
fmt.Fprintf(w, `<div class="container page">
|
||||
<div class="no-print" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:1px solid #e0e0e0;">
|
||||
<a href="/invoice" class="text-accent">← Back</a>
|
||||
<button onclick="window.print()" class="btn btn-primary">Print Invoice</button>
|
||||
</div>
|
||||
<div class="invoice">
|
||||
<div class="invoice-header">
|
||||
<div>
|
||||
<h1 class="invoice-title">Tax Invoice</h1>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<p>Invoice ID: %s</p>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<p>Date: %s</p>
|
||||
</div>
|
||||
</div>`, inv.HumanReadableID, strings.ReplaceAll(inv.CreatedAt, "T", " ")[0:10])
|
||||
|
||||
// Display company details above the invoice table
|
||||
if invoice.CompanyDetails != "" {
|
||||
fmt.Fprintf(w, `<div class="invoice-details-block" style="margin-bottom:1rem;"><strong>From:</strong><br>%s</div>`, invoice.CompanyDetails)
|
||||
}
|
||||
|
||||
if err := view.PrintableInvoice(invoice).Render(c.Request().Context(), w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Display buyer details and total amount in same section (50-50 split or 100% if no buyer)
|
||||
totalGST := invoice.TotalCGST + invoice.TotalSGST + invoice.TotalIGST
|
||||
hasBuyerInfo := invoice.BuyerName != "" || invoice.BuyerDetails != ""
|
||||
|
||||
if hasBuyerInfo {
|
||||
fmt.Fprint(w, `<div class="invoice-footer-section" style="display:flex;justify-content:space-between;gap:2rem;">`)
|
||||
// Left side: Billed To (50%)
|
||||
fmt.Fprint(w, `<div style="flex:1;"><strong>Billed To:</strong><br>`)
|
||||
if invoice.BuyerName != "" {
|
||||
fmt.Fprintf(w, `<span class="invoice-details-block">%s</span>`, invoice.BuyerName)
|
||||
}
|
||||
if invoice.BuyerDetails != "" {
|
||||
fmt.Fprintf(w, `<br><span class="invoice-details-block">%s</span>`, invoice.BuyerDetails)
|
||||
}
|
||||
fmt.Fprint(w, `</div>`)
|
||||
// Right side: Total Amount (50%)
|
||||
fmt.Fprintf(w, `<div style="flex:1;">
|
||||
<p style="margin: 0.5rem 0;"><strong>Total Amount (before GST):</strong><br>%s</p>
|
||||
<p style="margin: 0.5rem 0;"><strong>GST Amount:</strong><br>%s</p>
|
||||
</div>`, numberToWords(invoice.SubTotal), numberToWords(totalGST))
|
||||
} else {
|
||||
fmt.Fprint(w, `<div class="invoice-footer-section">`)
|
||||
// Total Amount takes 100%
|
||||
fmt.Fprintf(w, `<div>
|
||||
<p style="margin: 0.5rem 0;"><strong>Total Amount (before GST):</strong><br>%s</p>
|
||||
<p style="margin: 0.5rem 0;"><strong>GST Amount:</strong><br>%s</p>
|
||||
</div>`, numberToWords(invoice.SubTotal), numberToWords(totalGST))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `</div>`)
|
||||
|
||||
// Bank details (left) and QR code (right) in the same section
|
||||
fmt.Fprint(w, `<div class="invoice-footer-section" style="display:flex;justify-content:space-between;align-items:flex-start;">`)
|
||||
if invoice.BankDetails != "" {
|
||||
fmt.Fprintf(w, `<div style="flex:1;"><strong>Bank Details:</strong><br><span class="invoice-details-block">%s</span></div>`, invoice.BankDetails)
|
||||
} else {
|
||||
fmt.Fprint(w, `<div style="flex:1;"></div>`)
|
||||
}
|
||||
fmt.Fprintf(w, `<div style="margin-left:1rem;"><img src="data:image/png;base64,%s" alt="QR Code" style="width:80px;height:80px;"></div>`, qrBase64)
|
||||
fmt.Fprint(w, `</div>`)
|
||||
|
||||
fmt.Fprint(w, "</div>")
|
||||
|
||||
fmt.Fprint(w, "</div></body></html>")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddProductRowHandler returns HTML for a new product row (HTMX endpoint)
|
||||
func (h *BillingHandlers) AddProductRowHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
indexStr := c.QueryParam("index")
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
index = 0
|
||||
}
|
||||
|
||||
products, _ := h.db.GetAllProducts(userID)
|
||||
|
||||
// Build product options HTML
|
||||
productOptions := `<option value="">-- Select Product --</option>`
|
||||
for _, p := range products {
|
||||
productOptions += fmt.Sprintf(`<option value="%s">%s (₹%.2f)</option>`, p.SKU, p.Name, p.BasePrice)
|
||||
}
|
||||
|
||||
rowHTML := fmt.Sprintf(`
|
||||
<div class="product-row">
|
||||
<div class="product-row-grid">
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label">Product</label>
|
||||
<select name="product_sku_%d" class="form-select">%s</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin:0;">
|
||||
<label class="form-label">Qty</label>
|
||||
<input type="number" name="qty_%d" value="1" min="0" class="form-input">
|
||||
</div>
|
||||
<div style="padding-top:20px;">
|
||||
<button type="button" onclick="this.closest('.product-row').remove()" class="btn btn-danger btn-sm">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`, index, productOptions, index)
|
||||
|
||||
return c.HTML(http.StatusOK, rowHTML)
|
||||
}
|
||||
107
internal/handler/buyer.go
Normal file
107
internal/handler/buyer.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package handler
|
||||
|
||||
import "billit/internal/view"
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// BuyerHandlers holds db reference for buyer operations
|
||||
type BuyerHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewBuyerHandlers creates handlers with db access
|
||||
func NewBuyerHandlers(db database.Service) *BuyerHandlers {
|
||||
return &BuyerHandlers{db: db}
|
||||
}
|
||||
|
||||
// BuyerListHandler renders the /buyer page with all buyers
|
||||
func (h *BuyerHandlers) BuyerListHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
buyers, err := h.db.GetAllBuyerDetails(userID)
|
||||
if err != nil {
|
||||
return view.RenderServerError(c, "Failed to load buyers. Please try again.")
|
||||
}
|
||||
return view.Render(c, view.BuyerListPage(buyers))
|
||||
}
|
||||
|
||||
// BuyerCreatePageHandler renders the /buyer/create form page
|
||||
func (h *BuyerHandlers) BuyerCreatePageHandler(c echo.Context) error {
|
||||
return view.Render(c, view.BuyerCreatePage())
|
||||
}
|
||||
|
||||
// BuyerEditPageHandler renders the /buyer/edit/:id form page
|
||||
func (h *BuyerHandlers) BuyerEditPageHandler(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
userID := getUserID(c)
|
||||
|
||||
buyer, err := h.db.GetBuyerDetails(id, userID)
|
||||
if err != nil || buyer == nil {
|
||||
return view.RenderNotFound(c, "Buyer not found or you don't have access to it.")
|
||||
}
|
||||
return view.Render(c, view.BuyerEditPage(*buyer))
|
||||
}
|
||||
|
||||
// BuyerCreateHandler handles POST /buyer/create
|
||||
func (h *BuyerHandlers) BuyerCreateHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
name := c.FormValue("name")
|
||||
if name == "" {
|
||||
return c.String(http.StatusBadRequest, "Name is required")
|
||||
}
|
||||
|
||||
details := c.FormValue("details")
|
||||
|
||||
_, err := h.db.CreateBuyerDetails(userID, name, details)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, "Failed to create buyer")
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/buyer")
|
||||
}
|
||||
|
||||
// BuyerUpdateHandler handles POST /buyer/edit/:id
|
||||
func (h *BuyerHandlers) BuyerUpdateHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
name := c.FormValue("name")
|
||||
if name == "" {
|
||||
return c.String(http.StatusBadRequest, "Name is required")
|
||||
}
|
||||
|
||||
details := c.FormValue("details")
|
||||
|
||||
err := h.db.UpdateBuyerDetails(id, userID, name, details)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, "Failed to update buyer")
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/buyer")
|
||||
}
|
||||
|
||||
// BuyerDeleteHandler handles DELETE /buyer/:id
|
||||
func (h *BuyerHandlers) BuyerDeleteHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.String(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
err := h.db.DeleteBuyerDetails(id, userID)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, "Failed to delete buyer")
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
23
internal/handler/health.go
Normal file
23
internal/handler/health.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// HealthHandlers holds dependencies for health checks
|
||||
type HealthHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewHealthHandlers creates health handlers with db access
|
||||
func NewHealthHandlers(db database.Service) *HealthHandlers {
|
||||
return &HealthHandlers{db: db}
|
||||
}
|
||||
|
||||
// HealthHandler returns the health status
|
||||
func (h *HealthHandlers) HealthHandler(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, h.db.Health())
|
||||
}
|
||||
39
internal/handler/home.go
Normal file
39
internal/handler/home.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import "billit/internal/models"
|
||||
import "billit/internal/view"
|
||||
import (
|
||||
"billit/internal/database"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// HomeHandlers holds db reference for home page operations
|
||||
type HomeHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewHomeHandlers creates handlers with db access
|
||||
func NewHomeHandlers(db database.Service) *HomeHandlers {
|
||||
return &HomeHandlers{db: db}
|
||||
}
|
||||
|
||||
// HomePageHandler renders the home page with recent data
|
||||
func (h *HomeHandlers) HomePageHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
userEmail, _ := c.Get("user_email").(string)
|
||||
|
||||
// Get recent products (last 5)
|
||||
recentProducts, err := h.db.GetRecentProducts(userID, 5)
|
||||
if err != nil {
|
||||
recentProducts = []models.Product{}
|
||||
}
|
||||
|
||||
// Get recent invoices (last 5)
|
||||
recentInvoices, err := h.db.GetRecentInvoices(userID, 5)
|
||||
if err != nil {
|
||||
recentInvoices = []models.Invoice{}
|
||||
}
|
||||
|
||||
return view.Render(c, view.HomePage(userEmail, recentProducts, recentInvoices))
|
||||
}
|
||||
33
internal/handler/invoices.go
Normal file
33
internal/handler/invoices.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handler
|
||||
|
||||
import "billit/internal/view"
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// InvoicesHandlers holds db reference for invoice operations
|
||||
type InvoicesHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewInvoicesHandlers creates handlers with db access
|
||||
func NewInvoicesHandlers(db database.Service) *InvoicesHandlers {
|
||||
return &InvoicesHandlers{db: db}
|
||||
}
|
||||
|
||||
// InvoicesListHandler renders the /invoice page with all invoices
|
||||
func (h *InvoicesHandlers) InvoicesListHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
invoices, err := h.db.GetAllInvoices(userID)
|
||||
if err != nil {
|
||||
return view.RenderServerError(c, "Failed to load invoices. Please try again.")
|
||||
}
|
||||
return view.Render(c, view.InvoicesPage(invoices))
|
||||
}
|
||||
53
internal/handler/modal.go
Normal file
53
internal/handler/modal.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"billit/internal/view"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// ModalHandlers holds dependencies
|
||||
type ModalHandlers struct{}
|
||||
|
||||
// NewModalHandlers creates new handlers
|
||||
func NewModalHandlers() *ModalHandlers {
|
||||
return &ModalHandlers{}
|
||||
}
|
||||
|
||||
// ConfirmHandler renders the confirmation modal
|
||||
func (h *ModalHandlers) ConfirmHandler(c echo.Context) error {
|
||||
title := c.QueryParam("title")
|
||||
if title == "" {
|
||||
title = "Confirm Action"
|
||||
}
|
||||
|
||||
message := c.QueryParam("message")
|
||||
if message == "" {
|
||||
message = "Are you sure you want to proceed?"
|
||||
}
|
||||
|
||||
confirmText := c.QueryParam("confirm_text")
|
||||
if confirmText == "" {
|
||||
confirmText = "Confirm"
|
||||
}
|
||||
|
||||
url := c.QueryParam("url")
|
||||
method := c.QueryParam("method")
|
||||
if method == "" {
|
||||
method = "delete" // Default to delete
|
||||
}
|
||||
|
||||
target := c.QueryParam("target")
|
||||
|
||||
props := view.ModalProps{
|
||||
Title: title,
|
||||
Message: message,
|
||||
ConfirmText: confirmText,
|
||||
ConfirmURL: url,
|
||||
Method: strings.ToLower(method),
|
||||
Target: target,
|
||||
}
|
||||
|
||||
return view.Render(c, view.Modal(props))
|
||||
}
|
||||
233
internal/handler/product.go
Normal file
233
internal/handler/product.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package handler
|
||||
|
||||
import "billit/internal/models"
|
||||
import "billit/internal/view"
|
||||
import (
|
||||
"billit/internal/database"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// ProductHandlers holds db reference for product operations
|
||||
type ProductHandlers struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewProductHandlers creates handlers with db access
|
||||
func NewProductHandlers(db database.Service) *ProductHandlers {
|
||||
return &ProductHandlers{db: db}
|
||||
}
|
||||
|
||||
// getUserID extracts user ID from context (set by auth middleware)
|
||||
func getUserID(c echo.Context) string {
|
||||
if uid, ok := c.Get("user_id").(string); ok {
|
||||
return uid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ProductListHandler renders the /product page with all products
|
||||
func (h *ProductHandlers) ProductListHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
products, err := h.db.GetAllProducts(userID)
|
||||
if err != nil {
|
||||
return view.RenderServerError(c, "Failed to load products. Please try again.")
|
||||
}
|
||||
return view.Render(c, view.ProductListPage(products))
|
||||
}
|
||||
|
||||
// ProductCreatePageHandler renders the /product/create form page
|
||||
func (h *ProductHandlers) ProductCreatePageHandler(c echo.Context) error {
|
||||
return view.Render(c, view.ProductCreatePage())
|
||||
}
|
||||
|
||||
// ProductEditPageHandler renders the /product/edit/:sku form page
|
||||
func (h *ProductHandlers) ProductEditPageHandler(c echo.Context) error {
|
||||
sku := c.Param("sku")
|
||||
userID := getUserID(c)
|
||||
|
||||
product, err := h.db.GetProductBySKU(sku, userID)
|
||||
if err != nil || product == nil {
|
||||
return view.RenderNotFound(c, "Product not found or you don't have access to it.")
|
||||
}
|
||||
return view.Render(c, view.ProductEditPage(*product))
|
||||
}
|
||||
|
||||
// ProductCreateHandler handles POST /product/create
|
||||
func (h *ProductHandlers) ProductCreateHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
sku := c.FormValue("sku")
|
||||
if sku == "" {
|
||||
return c.String(http.StatusBadRequest, "SKU is required")
|
||||
}
|
||||
|
||||
name := c.FormValue("name")
|
||||
if name == "" {
|
||||
return c.String(http.StatusBadRequest, "Name is required")
|
||||
}
|
||||
|
||||
// Check if SKU already exists for this user
|
||||
existing, _ := h.db.GetProductBySKU(sku, userID)
|
||||
if existing != nil {
|
||||
return c.String(http.StatusBadRequest, "A product with this SKU already exists")
|
||||
}
|
||||
|
||||
hsn := c.FormValue("hsn")
|
||||
baseStr := c.FormValue("base_price")
|
||||
wholesaleStr := c.FormValue("wholesale_price")
|
||||
gstStr := c.FormValue("gst_rate")
|
||||
smallQtyStr := c.FormValue("small_order_qty")
|
||||
|
||||
base, _ := strconv.ParseFloat(baseStr, 64)
|
||||
wholesale, _ := strconv.ParseFloat(wholesaleStr, 64)
|
||||
if wholesale == 0 {
|
||||
wholesale = base // default wholesale to base price
|
||||
}
|
||||
|
||||
gstRate := 0.18 // default 18%
|
||||
switch gstStr {
|
||||
case "0":
|
||||
gstRate = 0.0
|
||||
case "5":
|
||||
gstRate = 0.05
|
||||
case "12":
|
||||
gstRate = 0.12
|
||||
case "18":
|
||||
gstRate = 0.18
|
||||
case "28":
|
||||
gstRate = 0.28
|
||||
}
|
||||
|
||||
smallQty := 1
|
||||
if v, err := strconv.Atoi(smallQtyStr); err == nil && v > 0 {
|
||||
smallQty = v
|
||||
}
|
||||
|
||||
smallFeeStr := c.FormValue("small_order_fee")
|
||||
smallFee, _ := strconv.ParseFloat(smallFeeStr, 64)
|
||||
|
||||
unit := c.FormValue("unit")
|
||||
if unit == "" {
|
||||
unit = "pcs"
|
||||
}
|
||||
|
||||
product := models.Product{
|
||||
SKU: sku,
|
||||
Name: name,
|
||||
HSNCode: hsn,
|
||||
BasePrice: base,
|
||||
WholesalePrice: wholesale,
|
||||
GSTRate: gstRate,
|
||||
SmallOrderQty: smallQty,
|
||||
SmallOrderFee: smallFee,
|
||||
Unit: unit,
|
||||
}
|
||||
|
||||
if err := h.db.CreateProduct(product, userID); err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to create product")
|
||||
}
|
||||
|
||||
// Redirect to product list
|
||||
return c.Redirect(http.StatusSeeOther, "/product")
|
||||
}
|
||||
|
||||
// ProductUpdateHandler handles POST /product/edit/:sku
|
||||
func (h *ProductHandlers) ProductUpdateHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
if userID == "" {
|
||||
return c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
sku := c.Param("sku")
|
||||
|
||||
// Verify product belongs to user
|
||||
existing, _ := h.db.GetProductBySKU(sku, userID)
|
||||
if existing == nil {
|
||||
return c.String(http.StatusNotFound, "Product not found")
|
||||
}
|
||||
|
||||
name := c.FormValue("name")
|
||||
if name == "" {
|
||||
return c.String(http.StatusBadRequest, "Name is required")
|
||||
}
|
||||
|
||||
hsn := c.FormValue("hsn")
|
||||
baseStr := c.FormValue("base_price")
|
||||
wholesaleStr := c.FormValue("wholesale_price")
|
||||
gstStr := c.FormValue("gst_rate")
|
||||
smallQtyStr := c.FormValue("small_order_qty")
|
||||
|
||||
base, _ := strconv.ParseFloat(baseStr, 64)
|
||||
wholesale, _ := strconv.ParseFloat(wholesaleStr, 64)
|
||||
if wholesale == 0 {
|
||||
wholesale = base
|
||||
}
|
||||
|
||||
gstRate := 0.18
|
||||
switch gstStr {
|
||||
case "0":
|
||||
gstRate = 0.0
|
||||
case "5":
|
||||
gstRate = 0.05
|
||||
case "12":
|
||||
gstRate = 0.12
|
||||
case "18":
|
||||
gstRate = 0.18
|
||||
case "28":
|
||||
gstRate = 0.28
|
||||
}
|
||||
|
||||
smallQty := 1
|
||||
if v, err := strconv.Atoi(smallQtyStr); err == nil && v > 0 {
|
||||
smallQty = v
|
||||
}
|
||||
|
||||
smallFeeStr := c.FormValue("small_order_fee")
|
||||
smallFee, _ := strconv.ParseFloat(smallFeeStr, 64)
|
||||
|
||||
unit := c.FormValue("unit")
|
||||
if unit == "" {
|
||||
unit = "pcs"
|
||||
}
|
||||
|
||||
product := models.Product{
|
||||
SKU: sku,
|
||||
Name: name,
|
||||
HSNCode: hsn,
|
||||
BasePrice: base,
|
||||
WholesalePrice: wholesale,
|
||||
GSTRate: gstRate,
|
||||
SmallOrderQty: smallQty,
|
||||
SmallOrderFee: smallFee,
|
||||
Unit: unit,
|
||||
}
|
||||
|
||||
if err := h.db.UpdateProduct(product, userID); err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to update product")
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/product")
|
||||
}
|
||||
|
||||
// ProductDeleteHandler handles DELETE /product/:sku
|
||||
func (h *ProductHandlers) ProductDeleteHandler(c echo.Context) error {
|
||||
userID := getUserID(c)
|
||||
sku := c.Param("sku")
|
||||
|
||||
if err := h.db.DeleteProduct(sku, userID); err != nil {
|
||||
return c.String(http.StatusInternalServerError, "failed to delete product")
|
||||
}
|
||||
|
||||
// For HTMX, return empty to remove the row
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/product")
|
||||
}
|
||||
Reference in New Issue
Block a user