Compare commits

..

No commits in common. "aa3b63095ca9de539091e5548a5865c4eb1d1751" and "c51da2a96580e49a5d8768ab44b27af43581ddbc" have entirely different histories.

22 changed files with 9 additions and 709 deletions

4
.gitignore vendored

@ -20,10 +20,6 @@ go.work
# Project specific binaries
erp_system
erp.db
erp.db-shm
erp.db-wal
erp_system
# SQLite
*.db
*.db-shm

@ -1,224 +0,0 @@
package main
import (
"fmt"
"log"
"math/rand"
"os"
"strconv"
"time"
"erp_system/internal/database"
_ "modernc.org/sqlite"
)
func main() {
dbPath := "erp.db"
if len(os.Args) > 1 {
dbPath = os.Args[1]
}
log.Printf("Connecting to database at %s...", dbPath)
// We can use the internal database package to initialize (runs migrations)
db, err := database.Initialize(dbPath)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()
// Configuration for generation
const (
numCustomers = 500
maxOrdersPerCust = 15
maxLinesPerOrder = 8
numJournalEntries = 100
)
log.Println("Starting data generation...")
start := time.Now()
tx, err := db.Begin()
if err != nil {
log.Fatalf("Failed to begin transaction: %v", err)
}
// 1. Customers
customerIDs := make([]int64, 0, numCustomers)
for i := 0; i < numCustomers; i++ {
name := fmt.Sprintf("Customer %s %s", randomString(5), randomString(7))
email := fmt.Sprintf("%s.%s@example.com", randomString(4), randomString(5))
phone := fmt.Sprintf("555-%04d", rand.Intn(10000))
address := fmt.Sprintf("%d %s St, City %s", rand.Intn(999), randomString(6), randomString(4))
res, err := tx.Exec("INSERT INTO customers (name, email, phone, address) VALUES (?, ?, ?, ?)", name, email, phone, address)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert customer: %v", err)
}
id, _ := res.LastInsertId()
customerIDs = append(customerIDs, id)
}
log.Printf("Generated %d customers", numCustomers)
// 2. Orders and Lines and Invoices
var orderCount, lineCount, invoiceCount int
for _, custID := range customerIDs {
numOrders := rand.Intn(maxOrdersPerCust)
for j := 0; j < numOrders; j++ {
// Create Order
status := "draft"
r := rand.Float32()
if r > 0.7 {
status = "fulfilled"
} else if r > 0.4 {
status = "confirmed"
} else if r > 0.3 {
status = "cancelled"
}
// Random date in last year
orderDate := time.Now().AddDate(0, 0, -rand.Intn(365))
res, err := tx.Exec("INSERT INTO orders (customer_id, status, order_date, notes) VALUES (?, ?, ?, ?)",
custID, status, orderDate.Format("2006-01-02"), "Generated order")
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert order: %v", err)
}
orderID, _ := res.LastInsertId()
orderCount++
// Create Order Lines
numLines := rand.Intn(maxLinesPerOrder) + 1
var totalAmount float64
for k := 0; k < numLines; k++ {
qty := float64(rand.Intn(10) + 1)
price := float64(rand.Intn(100) + 10)
desc := fmt.Sprintf("Product %s-%d", randomString(3), rand.Intn(100))
lineTotal := qty * price
totalAmount += lineTotal
_, err := tx.Exec("INSERT INTO order_lines (order_id, description, quantity, unit_price, line_total) VALUES (?, ?, ?, ?, ?)",
orderID, desc, qty, price, lineTotal)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert order line: %v", err)
}
lineCount++
}
// Update Order Total
_, err = tx.Exec("UPDATE orders SET total_amount = ? WHERE id = ?", totalAmount, orderID)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to update order total: %v", err)
}
// Create Invoice if order is confirmed or fulfilled
if status == "confirmed" || status == "fulfilled" {
invoiceStatus := "pending"
if rand.Float32() > 0.5 {
invoiceStatus = "paid"
}
invNum := fmt.Sprintf("INV-%d-%d", orderID, rand.Intn(10000))
dueDate := orderDate.AddDate(0, 1, 0) // Due in 30 days
var paidDate interface{} = nil
if invoiceStatus == "paid" {
pd := dueDate.AddDate(0, 0, -rand.Intn(10))
paidDate = pd.Format("2006-01-02")
}
_, err := tx.Exec("INSERT INTO invoices (order_id, customer_id, invoice_number, status, amount, due_date, paid_date) VALUES (?, ?, ?, ?, ?, ?, ?)",
orderID, custID, invNum, invoiceStatus, totalAmount, dueDate.Format("2006-01-02"), paidDate)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert invoice: %v", err)
}
invoiceCount++
}
}
}
log.Printf("Generated %d orders, %d lines, %d invoices", orderCount, lineCount, invoiceCount)
// 3. Journal Entries
// Fetch account IDs first
rows, err := tx.Query("SELECT id, code, type FROM gl_accounts")
if err != nil {
tx.Rollback()
log.Fatalf("Failed to fetch accounts: %v", err)
}
defer rows.Close()
type Account struct {
ID int64
Code string
Type string
}
var accounts []Account
for rows.Next() {
var a Account
if err := rows.Scan(&a.ID, &a.Code, &a.Type); err != nil {
log.Fatalf("Failed to scan account: %v", err)
}
accounts = append(accounts, a)
}
if len(accounts) >= 2 {
for i := 0; i < numJournalEntries; i++ {
date := time.Now().AddDate(0, 0, -rand.Intn(60)).Format("2006-01-02")
desc := fmt.Sprintf("Journal Entry %d", i+1)
res, err := tx.Exec("INSERT INTO journal_entries (entry_date, description, reference) VALUES (?, ?, ?)",
date, desc, "GEN-"+strconv.Itoa(i))
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert journal entry: %v", err)
}
jeID, _ := res.LastInsertId()
// Simple balanced entry: 2 lines
amount := float64(rand.Intn(5000) + 100)
// Pick 2 random accounts
idx1 := rand.Intn(len(accounts))
idx2 := rand.Intn(len(accounts))
for idx1 == idx2 {
idx2 = rand.Intn(len(accounts))
}
// Debit line
_, err = tx.Exec("INSERT INTO journal_lines (journal_entry_id, account_id, debit, credit) VALUES (?, ?, ?, ?)",
jeID, accounts[idx1].ID, amount, 0)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert journal line 1: %v", err)
}
// Credit line
_, err = tx.Exec("INSERT INTO journal_lines (journal_entry_id, account_id, debit, credit) VALUES (?, ?, ?, ?)",
jeID, accounts[idx2].ID, 0, amount)
if err != nil {
tx.Rollback()
log.Fatalf("Failed to insert journal line 2: %v", err)
}
}
}
log.Printf("Generated %d journal entries", numJournalEntries)
if err := tx.Commit(); err != nil {
log.Fatalf("Failed to commit transaction: %v", err)
}
log.Printf("Seeding completed in %v", time.Since(start))
}
func randomString(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

BIN
erp.db-shm Normal file

Binary file not shown.

BIN
erp.db-wal Normal file

Binary file not shown.

BIN
erp_system Executable file

Binary file not shown.

@ -9,32 +9,18 @@ import (
func (h *Handler) CustomerList(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit := 10
customers, total, err := models.CustomerGetPaginated(h.DB, search, page, limit)
customers, err := models.CustomerGetAll(h.DB, search)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
data := map[string]interface{}{
"Title": "Customers",
"Username": h.getUsername(r),
"ActivePage": "customers",
"Customers": customers,
"Search": search,
"Page": page,
"TotalPages": totalPages,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
}
// HTMX partial for search

@ -10,32 +10,18 @@ import (
func (h *Handler) InvoiceList(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit := 10
invoices, total, err := models.InvoiceGetPaginated(h.DB, status, page, limit)
invoices, err := models.InvoiceGetAll(h.DB, status)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
data := map[string]interface{}{
"Title": "Invoices",
"Username": h.getUsername(r),
"ActivePage": "invoices",
"Invoices": invoices,
"FilterStatus": status,
"Page": page,
"TotalPages": totalPages,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
}
if r.Header.Get("HX-Request") == "true" && r.URL.Query().Get("partial") == "true" {

@ -25,31 +25,17 @@ func (h *Handler) ChartOfAccounts(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) JournalEntries(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit := 20
entries, total, err := models.JournalEntryGetPaginated(h.DB, page, limit)
entries, err := models.JournalEntryGetAll(h.DB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
data := map[string]interface{}{
"Title": "Journal Entries",
"Username": h.getUsername(r),
"ActivePage": "ledger",
"Entries": entries,
"Page": page,
"TotalPages": totalPages,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
}
h.render(w, []string{"layout.html", "ledger/journal_entries.html"}, data)
}

@ -11,32 +11,18 @@ import (
func (h *Handler) OrderList(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit := 10
orders, total, err := models.OrderGetPaginated(h.DB, status, page, limit)
orders, err := models.OrderGetAll(h.DB, status)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalPages := (total + limit - 1) / limit
data := map[string]interface{}{
"Title": "Orders",
"Username": h.getUsername(r),
"ActivePage": "orders",
"Orders": orders,
"FilterStatus": status,
"Page": page,
"TotalPages": totalPages,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
}
if r.Header.Get("HX-Request") == "true" && r.URL.Query().Get("partial") == "true" {

@ -84,46 +84,3 @@ func CustomerCount(db *sql.DB) int {
db.QueryRow("SELECT COUNT(*) FROM customers").Scan(&count)
return count
}
func CustomerGetPaginated(db *sql.DB, search string, page, limit int) ([]Customer, int, error) {
offset := (page - 1) * limit
// Base queries
query := "SELECT id, name, email, phone, address, created_at, updated_at FROM customers"
countQuery := "SELECT COUNT(*) FROM customers"
args := []interface{}{}
if search != "" {
where := " WHERE name LIKE ? OR email LIKE ?"
query += where
countQuery += where
s := "%" + search + "%"
args = append(args, s, s)
}
// Get total count first
var total int
if err := db.QueryRow(countQuery, args...).Scan(&total); err != nil {
return nil, 0, err
}
// Add limit and offset to query
query += " ORDER BY name LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var customers []Customer
for rows.Next() {
var c Customer
if err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Address, &c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, 0, err
}
customers = append(customers, c)
}
return customers, total, nil
}

@ -46,48 +46,6 @@ func InvoiceGetAll(db *sql.DB, status string) ([]Invoice, error) {
return invoices, nil
}
func InvoiceGetPaginated(db *sql.DB, status string, page, limit int) ([]Invoice, int, error) {
offset := (page - 1) * limit
// Base queries
query := `SELECT i.id, i.order_id, i.customer_id, c.name, i.invoice_number, i.status, i.amount, i.due_date, i.paid_date, i.created_at
FROM invoices i JOIN customers c ON i.customer_id = c.id`
countQuery := "SELECT COUNT(*) FROM invoices i"
args := []interface{}{}
if status != "" {
where := " WHERE i.status = ?"
query += where
countQuery += where
args = append(args, status)
}
// Get total count
var total int
if err := db.QueryRow(countQuery, args...).Scan(&total); err != nil {
return nil, 0, err
}
query += " ORDER BY i.created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var invoices []Invoice
for rows.Next() {
var inv Invoice
if err := rows.Scan(&inv.ID, &inv.OrderID, &inv.CustomerID, &inv.CustomerName, &inv.InvoiceNumber, &inv.Status, &inv.Amount, &inv.DueDate, &inv.PaidDate, &inv.CreatedAt); err != nil {
return nil, 0, err
}
invoices = append(invoices, inv)
}
return invoices, total, nil
}
func InvoiceGetByID(db *sql.DB, id int) (*Invoice, error) {
inv := &Invoice{}
err := db.QueryRow(

@ -103,47 +103,6 @@ func JournalEntryGetAll(db *sql.DB) ([]JournalEntry, error) {
return entries, nil
}
func JournalEntryGetPaginated(db *sql.DB, page, limit int) ([]JournalEntry, int, error) {
offset := (page - 1) * limit
var total int
if err := db.QueryRow("SELECT COUNT(*) FROM journal_entries").Scan(&total); err != nil {
return nil, 0, err
}
query := `
SELECT
je.id,
je.entry_date,
je.description,
je.reference,
je.created_at,
COALESCE(SUM(jl.debit), 0),
COALESCE(SUM(jl.credit), 0)
FROM journal_entries je
LEFT JOIN journal_lines jl ON je.id = jl.journal_entry_id
GROUP BY je.id
ORDER BY je.created_at DESC
LIMIT ? OFFSET ?
`
rows, err := db.Query(query, limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var entries []JournalEntry
for rows.Next() {
var je JournalEntry
if err := rows.Scan(&je.ID, &je.EntryDate, &je.Description, &je.Reference, &je.CreatedAt, &je.TotalDebit, &je.TotalCredit); err != nil {
return nil, 0, err
}
entries = append(entries, je)
}
return entries, total, nil
}
func JournalEntryGetByID(db *sql.DB, id int) (*JournalEntry, error) {
je := &JournalEntry{}
err := db.QueryRow("SELECT id, entry_date, description, reference, created_at FROM journal_entries WHERE id = ?", id).

@ -55,48 +55,6 @@ func OrderGetAll(db *sql.DB, status string) ([]Order, error) {
return orders, nil
}
func OrderGetPaginated(db *sql.DB, status string, page, limit int) ([]Order, int, error) {
offset := (page - 1) * limit
// Base queries
query := `SELECT o.id, o.customer_id, c.name, o.status, o.order_date, o.total_amount, o.notes, o.created_at, o.updated_at
FROM orders o JOIN customers c ON o.customer_id = c.id`
countQuery := "SELECT COUNT(*) FROM orders o"
args := []interface{}{}
if status != "" {
where := " WHERE o.status = ?"
query += where
countQuery += where
args = append(args, status)
}
// Get total count
var total int
if err := db.QueryRow(countQuery, args...).Scan(&total); err != nil {
return nil, 0, err
}
query += " ORDER BY o.created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var orders []Order
for rows.Next() {
var o Order
if err := rows.Scan(&o.ID, &o.CustomerID, &o.CustomerName, &o.Status, &o.OrderDate, &o.TotalAmount, &o.Notes, &o.CreatedAt, &o.UpdatedAt); err != nil {
return nil, 0, err
}
orders = append(orders, o)
}
return orders, total, nil
}
func OrderGetByID(db *sql.DB, id int) (*Order, error) {
o := &Order{}
err := db.QueryRow(

@ -45,10 +45,6 @@ func main() {
mux := http.NewServeMux()
// Static/public routes
mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources"))))
mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "resources/favicon.png")
})
mux.HandleFunc("GET /login", h.LoginPage)
mux.HandleFunc("POST /login", h.LoginSubmit)
mux.HandleFunc("POST /logout", h.Logout)

@ -1,42 +0,0 @@
package main_test
import (
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestResources_Favicon(t *testing.T) {
// Create resources directory and dummy favicon if not exists
if _, err := os.Stat("resources"); os.IsNotExist(err) {
os.Mkdir("resources", 0755)
}
if _, err := os.Stat("resources/favicon.png"); os.IsNotExist(err) {
os.WriteFile("resources/favicon.png", []byte("dummy"), 0644)
defer os.Remove("resources/favicon.png")
}
// Setup handler exactly as in main.go
mux := http.NewServeMux()
mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources"))))
mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "resources/favicon.png")
})
// Test request for resources path
req := httptest.NewRequest("GET", "/resources/favicon.png", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("GET /resources/favicon.png failed: %d", w.Code)
}
// Test request for favicon.ico
reqIco := httptest.NewRequest("GET", "/favicon.ico", nil)
wIco := httptest.NewRecorder()
mux.ServeHTTP(wIco, reqIco)
if wIco.Code != http.StatusOK {
t.Errorf("GET /favicon.ico failed: %d", wIco.Code)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

@ -60,40 +60,4 @@
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Page <span class="font-medium">{{.Page}}</span> of <span class="font-medium">{{.TotalPages}}</span>
</p>
</div>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{{if .HasPrev}}
<button
hx-get="/customers?page={{.PrevPage}}&search={{.Search}}&partial=true"
hx-target="#customer-table"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Previous</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
{{if .HasNext}}
<button
hx-get="/customers?page={{.NextPage}}&search={{.Search}}&partial=true"
hx-target="#customer-table"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Next</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}

@ -64,40 +64,4 @@
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Page <span class="font-medium">{{.Page}}</span> of <span class="font-medium">{{.TotalPages}}</span>
</p>
</div>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{{if .HasPrev}}
<button
hx-get="/invoices?page={{.PrevPage}}&status={{.FilterStatus}}&partial=true"
hx-target="#invoice-table"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Previous</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
{{if .HasNext}}
<button
hx-get="/invoices?page={{.NextPage}}&status={{.FilterStatus}}&partial=true"
hx-target="#invoice-table"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Next</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}

@ -4,8 +4,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="64x64" href="/resources/favicon.png?v=1">
<title>{{.Title}} - ERP System</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://cdn.tailwindcss.com"></script>
@ -23,17 +21,17 @@
<div class="flex h-16 items-center justify-between">
<div class="flex items-center">
<a href="/" class="text-white font-bold text-xl tracking-tight">ERP System</a>
<div class="hidden md:flex ml-10 items-baseline space-x-4">
<div class="ml-10 flex items-baseline space-x-4">
<a href="/" class="{{if eq .ActivePage "dashboard"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Dashboard</a>
<a href="/customers" class="{{if eq .ActivePage "customers"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Customers</a>
<a href="/orders" class="{{if eq .ActivePage "orders"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Orders</a>
<a href="/invoices" class="{{if eq .ActivePage "invoices"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Invoices</a>
<div class="relative">
<button type="button" onclick="toggleDropdown(event, 'ledger-dropdown')" class="{{if eq .ActivePage "ledger"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium inline-flex items-center">
<div class="relative group">
<button class="{{if eq .ActivePage "ledger"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium inline-flex items-center">
General Ledger
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</button>
<div id="ledger-dropdown" class="absolute left-0 z-10 mt-1 w-48 origin-top-left rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 hidden">
<div class="absolute left-0 z-10 mt-1 w-48 origin-top-left rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 hidden group-hover:block">
<div class="py-1">
<a href="/ledger/accounts" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" hx-boost="true">Chart of Accounts</a>
<a href="/ledger/journal" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" hx-boost="true">Journal Entries</a>
@ -43,52 +41,12 @@
</div>
</div>
</div>
<div class="hidden md:flex items-center space-x-4">
<div class="flex items-center space-x-4">
<span class="text-indigo-200 text-sm">{{.Username}}</span>
<form method="POST" action="/logout" hx-post="/logout">
<button type="submit" class="text-indigo-200 hover:text-white text-sm font-medium">Logout</button>
</form>
</div>
<div class="-mr-2 flex md:hidden">
<button type="button" onclick="toggleMobileMenu()" class="inline-flex items-center justify-center rounded-md bg-indigo-600 p-2 text-indigo-200 hover:bg-indigo-500 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600" aria-controls="mobile-menu" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
</div>
</div>
</div>
<div class="hidden md:hidden" id="mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2 sm:px-3">
<a href="/" class="{{if eq .ActivePage "dashboard"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Dashboard</a>
<a href="/customers" class="{{if eq .ActivePage "customers"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Customers</a>
<a href="/orders" class="{{if eq .ActivePage "orders"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Orders</a>
<a href="/invoices" class="{{if eq .ActivePage "invoices"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Invoices</a>
<div>
<button type="button" onclick="toggleDropdown(event, 'ledger-mobile-dropdown')" class="{{if eq .ActivePage "ledger"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} w-full text-left block rounded-md px-3 py-2 text-base font-medium flex justify-between items-center">
General Ledger
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</button>
<div id="ledger-mobile-dropdown" class="hidden pl-4 space-y-1">
<a href="/ledger/accounts" class="text-indigo-200 hover:bg-indigo-500 hover:text-white block rounded-md px-3 py-2 text-sm font-medium">Chart of Accounts</a>
<a href="/ledger/journal" class="text-indigo-200 hover:bg-indigo-500 hover:text-white block rounded-md px-3 py-2 text-sm font-medium">Journal Entries</a>
<a href="/ledger/trial-balance" class="text-indigo-200 hover:bg-indigo-500 hover:text-white block rounded-md px-3 py-2 text-sm font-medium">Trial Balance</a>
</div>
</div>
</div>
<div class="border-t border-indigo-700 pb-3 pt-4">
<div class="flex items-center px-5">
<div class="ml-3">
<div class="text-base font-medium leading-none text-white">{{.Username}}</div>
</div>
</div>
<div class="mt-3 space-y-1 px-2">
<form method="POST" action="/logout" hx-post="/logout">
<button type="submit" class="block w-full text-left rounded-md px-3 py-2 text-base font-medium text-indigo-200 hover:bg-indigo-500 hover:text-white">Logout</button>
</form>
</div>
</div>
</div>
</nav>
@ -100,25 +58,6 @@
</div>
</main>
</div>
<script>
function toggleDropdown(event, id) {
event.stopPropagation();
const el = document.getElementById(id);
if (el) el.classList.toggle('hidden');
}
function toggleMobileMenu() {
const el = document.getElementById('mobile-menu');
if (el) el.classList.toggle('hidden');
}
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('ledger-dropdown');
if (dropdown && !dropdown.classList.contains('hidden') && !event.target.closest('button[onclick*="ledger-dropdown"]') && !dropdown.contains(event.target)) {
dropdown.classList.add('hidden');
}
});
</script>
</body>
</html>
{{end}}

@ -45,37 +45,5 @@
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Page <span class="font-medium">{{.Page}}</span> of <span class="font-medium">{{.TotalPages}}</span>
</p>
</div>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{{if .HasPrev}}
<a href="/ledger/journal?page={{.PrevPage}}"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Previous</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
</svg>
</a>
{{end}}
{{if .HasNext}}
<a href="/ledger/journal?page={{.NextPage}}"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Next</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
</a>
{{end}}
</nav>
</div>
</div>
</div>
</div>
{{end}}

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/resources/favicon.png">
<title>Login - ERP System</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://cdn.tailwindcss.com"></script>

@ -66,40 +66,4 @@
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Page <span class="font-medium">{{.Page}}</span> of <span class="font-medium">{{.TotalPages}}</span>
</p>
</div>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
{{if .HasPrev}}
<button
hx-get="/orders?page={{.PrevPage}}&status={{.FilterStatus}}&partial=true"
hx-target="#order-table"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Previous</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
{{if .HasNext}}
<button
hx-get="/orders?page={{.NextPage}}&status={{.FilterStatus}}&partial=true"
hx-target="#order-table"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
<span class="sr-only">Next</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
</svg>
</button>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}