Compare commits

..

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

11 changed files with 14 additions and 211 deletions

36
.gitignore vendored

@ -1,36 +0,0 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Project specific binaries
erp_system
# SQLite
*.db
*.db-shm
*.db-wal
# OS Specific
.DS_Store
Thumbs.db
# Editor/IDE specific
.idea/
.vscode/
*.swp
*~

@ -1,97 +0,0 @@
# ERP System Agent Guidelines
This document provides instructions for coding agents working on the ERP System repository.
## 1. Build, Lint, and Test
### Build
To build the application:
```bash
go build -o erp_system main.go
```
To run the application locally:
```bash
go run main.go
```
The server defaults to port `1337`.
### Test
Run all tests:
```bash
go test ./...
```
Run a specific test package:
```bash
go test ./internal/handlers/...
```
Run a single specific test function:
```bash
# Pattern: go test -v <package_path> -run <TestNameRegex>
go test -v ./internal/handlers -run TestLoginSubmit
```
### Lint / Verify
Run standard Go vetting:
```bash
go vet ./...
```
Format code (always run before committing):
```bash
go fmt ./...
```
## 2. Code Style & Conventions
### General
- **Language:** Go 1.25.6+
- **Formatting:** Strictly adhere to `gofmt` standards.
- **Naming:**
- Use `PascalCase` for exported structs, fields, and functions.
- Use `camelCase` for unexported (private) identifiers.
- Keep variable names short but descriptive (e.g., `req` vs `r` is fine if context is clear, but `userRequest` is better for complex scopes).
### Project Structure
- `main.go`: Application entry point, router setup, dependency injection.
- `internal/`: Private application code.
- `database/`: DB initialization and helpers.
- `handlers/`: HTTP handlers.
- `models/`: Data structs and business logic/DB queries.
- `middleware/`: HTTP middleware (Auth, etc.).
- `templates/`: HTML templates (server-side rendered).
### Imports
Group imports into three blocks separated by a newline:
1. Standard library (e.g., `"net/http"`, `"os"`)
2. Third-party packages (e.g., `"github.com/gorilla/sessions"`)
3. Internal project packages (e.g., `"erp_system/internal/models"`)
Example:
```go
import (
"log"
"net/http"
"github.com/gorilla/sessions"
"erp_system/internal/models"
)
```
### Error Handling
- **Explicit Checks:** Always check errors. `if err != nil { ... }`.
- **Handlers:** Use `http.Error(w, err.Error(), http.Status...)` to return errors to the client.
- **Logging:** Log critical errors on the server side using `log.Printf` or similar before returning HTTP 500.
### HTTP & Routing
- Use Go's standard `net/http` `ServeMux` with Go 1.22+ routing patterns (e.g., `mux.Handle("GET /path/{id}", ...)`).
- **HTMX:** The application uses HTMX.
- Check for `HX-Request` header to differentiate full page loads vs partials.
- Use `HX-Redirect` header for client-side redirection when handling HTMX requests.
- Render partial templates when appropriate.
### Database
- Use `internal/models` for all database interactions.
- Pass the database connection (`*sql.DB`) or a wrapping struct to handlers via dependency injection (see `Handler` struct in `main.go`).
## 3. Cursor / Copilot Rules
(No specific Cursor or Copilot rules found in the repository yet. Adhere to the conventions above.)

Binary file not shown.

Binary file not shown.

@ -1,56 +0,0 @@
package handlers_test
import (
"net/http/httptest"
"os"
"testing"
"time"
"erp_system/internal/database"
"erp_system/internal/handlers"
"github.com/gorilla/sessions"
)
func TestJournalEntries_List(t *testing.T) {
// Setup temporary DB
dbFile := "test_ledger.db"
db, err := database.Initialize(dbFile)
if err != nil {
t.Fatalf("Failed to initialize DB: %v", err)
}
defer func() {
db.Close()
os.Remove(dbFile)
}()
// Setup Handler
store := sessions.NewCookieStore([]byte("secret"))
h := &handlers.Handler{
DB: db,
Store: store,
}
// Insert a dummy entry so the loop runs
db.Exec("INSERT INTO journal_entries (description) VALUES ('Test')")
// Create a timeout channel to detect freeze
done := make(chan bool)
go func() {
req := httptest.NewRequest("GET", "/ledger/journal", nil)
w := httptest.NewRecorder()
// Execute Handler
h.JournalEntries(w, req)
done <- true
}()
select {
case <-done:
t.Log("Handler completed successfully")
case <-time.After(2 * time.Second):
t.Fatal("Handler timed out - DEADLOCK DETECTED")
}
}

@ -73,20 +73,7 @@ func GLAccountGetByID(db *sql.DB, id int) (*GLAccount, error) {
} }
func JournalEntryGetAll(db *sql.DB) ([]JournalEntry, error) { func JournalEntryGetAll(db *sql.DB) ([]JournalEntry, error) {
rows, err := db.Query(` rows, err := db.Query("SELECT id, entry_date, description, reference, created_at FROM journal_entries ORDER BY created_at DESC")
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
`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,9 +82,14 @@ func JournalEntryGetAll(db *sql.DB) ([]JournalEntry, error) {
var entries []JournalEntry var entries []JournalEntry
for rows.Next() { for rows.Next() {
var je JournalEntry var je JournalEntry
if err := rows.Scan(&je.ID, &je.EntryDate, &je.Description, &je.Reference, &je.CreatedAt, &je.TotalDebit, &je.TotalCredit); err != nil { if err := rows.Scan(&je.ID, &je.EntryDate, &je.Description, &je.Reference, &je.CreatedAt); err != nil {
return nil, err return nil, err
} }
// Get totals
db.QueryRow("SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0) FROM journal_lines WHERE journal_entry_id = ?", je.ID).
Scan(&je.TotalDebit, &je.TotalCredit)
entries = append(entries, je) entries = append(entries, je)
} }
return entries, nil return entries, nil

@ -92,7 +92,7 @@ func main() {
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "1337" port = "8080"
} }
log.Printf("ERP System starting on http://localhost:%s", port) log.Printf("ERP System starting on http://localhost:%s", port)

@ -14,9 +14,9 @@
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white shadow sm:rounded-lg">
<form class="space-y-6 p-6" <form class="space-y-6 p-6"
{{if .IsNew}} {{if .IsNew}}
method="POST" action="/customers" hx-post="/customers" hx-target="body" method="POST" action="/customers" hx-post="/customers"
{{else}} {{else}}
method="POST" action="/customers/{{.Customer.ID}}" hx-put="/customers/{{.Customer.ID}}" hx-target="body" method="POST" action="/customers/{{.Customer.ID}}" hx-put="/customers/{{.Customer.ID}}"
{{end}}> {{end}}>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">

@ -12,7 +12,7 @@
{{end}} {{end}}
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white shadow sm:rounded-lg">
<form class="space-y-6 p-6" method="POST" action="/ledger/journal" id="je-form"> <form class="space-y-6 p-6" method="POST" action="/ledger/journal" hx-post="/ledger/journal" id="je-form">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div> <div>
<label for="entry_date" class="block text-sm font-medium text-gray-700">Date *</label> <label for="entry_date" class="block text-sm font-medium text-gray-700">Date *</label>

@ -23,7 +23,7 @@
</div> </div>
{{end}} {{end}}
<form class="mt-8 space-y-6" method="POST" action="/login" hx-post="/login" hx-target="body"> <form class="mt-8 space-y-6" method="POST" action="/login" hx-post="/login">
<div class="space-y-4 rounded-md shadow-sm"> <div class="space-y-4 rounded-md shadow-sm">
<div> <div>
<label for="username" class="block text-sm font-medium text-gray-700">Username</label> <label for="username" class="block text-sm font-medium text-gray-700">Username</label>

@ -14,9 +14,9 @@
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white shadow sm:rounded-lg">
<form class="space-y-6 p-6" <form class="space-y-6 p-6"
{{if .IsNew}} {{if .IsNew}}
method="POST" action="/orders" hx-post="/orders" hx-target="body" method="POST" action="/orders" hx-post="/orders"
{{else}} {{else}}
method="POST" action="/orders/{{.Order.ID}}" hx-put="/orders/{{.Order.ID}}" hx-target="body" method="POST" action="/orders/{{.Order.ID}}" hx-put="/orders/{{.Order.ID}}"
{{end}}> {{end}}>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">