diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..e33152e --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +name: 'agendas-db' +services: + db: + image: 'postgres:16' + environment: + POSTGRES_USER: "${DB_USER:?}" + POSTGRES_PASSWORD: "${DB_PASSWORD:?}" + POSTGRES_DATABASE: "${DB_DATABASE:?}" + restart: 'unless-stopped' + ports: + - '5432:5432' + volumes: + - 'db-data:/var/lib/postgresql/data/' +volumes: + db-data: diff --git a/db.go b/db.go new file mode 100644 index 0000000..4274fd1 --- /dev/null +++ b/db.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type DBClient struct { + Pool *pgxpool.Pool +} + +func (d DBClient) Ping(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if d.Pool == nil { + return fmt.Errorf("db: cannot check health using nil connection pool") + } + + return d.Pool.Ping(ctx) + } +} + +func (d DBClient) Init(ctx context.Context) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO Init + return fmt.Errorf("db: Init not implemented") +} + +func (d DBClient) CreateTransaction(ctx context.Context, transaction Transaction) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO CreateTransaction + return fmt.Errorf("db: CreateTransaction not implemented") +} + +func (d DBClient) ReadTransaction(ctx context.Context, membreID string, isPerpetual bool) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO ReadTransaction + return fmt.Errorf("db: ReadTransaction not implemented") +} + +func (d DBClient) CountTransactions(ctx context.Context) (membresSansAgenda, membresAvecPerpetuel, membresAvecNonPerpetuel, membresAvecTout int, err error) { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO CountTransactions + return 0, 0, 0, 0, fmt.Errorf("db: CountTransactions not implemented") +} diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..3999d7d --- /dev/null +++ b/entity.go @@ -0,0 +1,9 @@ +package main + +import "time" + +type Transaction struct { + MembreID string `db:"membre_id"` + GivenAt *time.Time `db:"given_at"` + IsPerpetual bool `db:"is_perpetual"` +} diff --git a/handler.go b/handler.go index 34f5ee2..a771687 100644 --- a/handler.go +++ b/handler.go @@ -11,11 +11,12 @@ import ( ) // Handling -func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFunc { +func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc { return func(c echo.Context) error { type data struct { Error string BottinHealthResponse bottin.ReadHealthResponse + IsDBUp bool } return c.Render(http.StatusOK, "index", func() (d data) { @@ -36,6 +37,16 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFu } d.BottinHealthResponse = bottinHealthResponse + // Check db health + if dbClient == nil { + return fmt.Errorf("Impossible de contacter la base de données, le client DB est nil") + } + + if err := dbClient.Ping(ctx); err != nil { + return err + } + d.IsDBUp = true + // No errors return nil } @@ -91,3 +102,66 @@ func UIReadMembre(ctx context.Context, bottinClient *bottin.APIClient) echo.Hand }()) } } + +/* +UICreateTransaction gère la création des transactions + +TODO: + - Fully implement + - Check context is not closed +*/ +func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc { + return func(c echo.Context) error { + type data struct { + BottinHealth bottin.ReadHealthResponse + Error string + Result string + } + + return c.Render(http.StatusOK, "index", func() (d data) { + if err := func() error { + if bottinClient == nil { + return fmt.Errorf("Cannot operate on nil *bottin.APIClient") + } + + bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx) + if err != nil { + return err + } + + d.BottinHealth = bottinReadHealthResponse + + isPerpetual := c.FormValue("is_perpetual") == "on" + membreID := c.FormValue("membre_id") + + if membreID == "" { + return fmt.Errorf("👎 Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.") + } + + // dbclient.CreateTransaction + if err := dbClient.CreateTransaction(ctx, Transaction{ + MembreID: membreID, + IsPerpetual: isPerpetual, + }); err != nil { + return err + } + + // Prepare result message + var typeAgenda string + if isPerpetual { + typeAgenda = "perpétuel" + } else { + typeAgenda = "non-perpétuel" + } + + d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", membreID, typeAgenda) + + return fmt.Errorf("UIIndexPOST not fully implemented") + }(); err != nil { + d.Error = err.Error() + log.Println("err:", d.Error) + } + return + }()) + } +} diff --git a/main.go b/main.go index d55e62b..9049128 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "codeberg.org/vlbeaudoin/voki/v3" "git.agecem.com/bottin/bottin/v10/pkg/bottin" + "github.com/jackc/pgx/v5/pgxpool" "golang.org/x/term" ) @@ -57,7 +58,16 @@ func run(ctx context.Context, cfg Config) error { protocol, )} - if err := RunServer(ctx, &bottinClient); err != nil && err != http.ErrServerClosed { + // connect to db + dbPool, err := pgxpool.New(ctx, "postgres://agendas:agendas@localhost:5432/agendas") + if err != nil { + return err + } + defer dbPool.Close() + + dbClient := DBClient{Pool: dbPool} + + if err := RunServer(ctx, cfg, &bottinClient, &dbClient); err != nil && err != http.ErrServerClosed { return err } diff --git a/render.go b/render.go deleted file mode 100644 index a3ebbc6..0000000 --- a/render.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "io" - "text/template" - - "git.agecem.com/bottin/agendas/ui" - "github.com/labstack/echo/v4" -) - -type Renderer struct { - templates *template.Template -} - -func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -func NewRenderer() *Renderer { - return &Renderer{ - templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")), - } -} diff --git a/server.go b/server.go index 93c2352..199abda 100644 --- a/server.go +++ b/server.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "git.agecem.com/bottin/agendas/ui" "git.agecem.com/bottin/bottin/v10/pkg/bottin" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) -func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { +func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) error { select { case <-ctx.Done(): return ctx.Err() @@ -18,15 +19,24 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { return fmt.Errorf("nil bottin client") } + if dbClient == nil { + return fmt.Errorf("nil dbClient") + } + e := echo.New() - e.Renderer = NewRenderer() + e.Renderer = ui.NewRenderer() e.Pre(middleware.AddTrailingSlash()) - e.GET("/", UIIndex(ctx, bottinClient)) + //basicauth TODO + //e.Use( - e.GET("/membre/", UIReadMembre(ctx, bottinClient)) + e.GET("/", UIIndex(ctx, bottinClient, dbClient)) + //e.GET("/transaction/", UIReadTransaction + e.POST("/transaction/", UICreateTransaction(ctx, cfg, bottinClient, dbClient)) + + //e.GET("/membre/", UIReadMembre(ctx, bottinClient)) return e.Start(":3333") } diff --git a/ui/embed.go b/ui/embed.go index c10c63b..fa6bb1b 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -1,6 +1,12 @@ package ui -import "embed" +import ( + "embed" + "io" + "text/template" + + "github.com/labstack/echo/v4" +) //go:embed *.html var htmlFS embed.FS @@ -8,3 +14,17 @@ var htmlFS embed.FS func HTMLFS() embed.FS { return htmlFS } + +type Renderer struct { + templates *template.Template +} + +func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +func NewRenderer() *Renderer { + return &Renderer{ + templates: template.Must(template.ParseFS(HTMLFS(), "*html")), + } +} diff --git a/ui/index.html b/ui/index.html index 7e43017..0583275 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,4 +1,122 @@ {{ define "index" }} -hello world -{{ . }} + + + + + + AGECEM | Agenda + + + + + + +

+ Distribution d'agendas aux membres de l'AGECEM +

+ +

+ 1) Si l'agenda demandé est perpétuel, cochez 'Perpétuel?:'
+
+ 2) Sélectionnez le champs 'Numéro étudiant:'
+
+ 3) Scannez la carte étudiante d'unE membre
+ -ou-
+ Entrez manuellement le code à 7 chiffres
+
+ 4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda +

+ +
+ +
+ +
+ + + + +

{{ . }}

+ + + {{ end }}