Compare commits
1 commit
main
...
feature/sw
| Author | SHA1 | Date | |
|---|---|---|---|
|
31f5ba1338 |
85 changed files with 5645 additions and 5598 deletions
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"bracketSpacing": true,
|
||||
"bracketSpacing": false,
|
||||
"bracketSameLine": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": true,
|
||||
"printWidth": 160
|
||||
"singleAttributePerLine": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3
|
||||
FROM alpine
|
||||
|
||||
COPY ./budget-go /usr/bin/
|
||||
COPY ./budget-go-client /usr/bin/
|
||||
|
|
|
|||
10
Makefile
10
Makefile
|
|
@ -46,6 +46,16 @@ build: tpl front rice
|
|||
-tags=static_build \
|
||||
-o $(EXECUTABLE_CMD) ./cmd/cli
|
||||
|
||||
.ONESHELL:
|
||||
swagger:
|
||||
directories="cmd/server,database/model"
|
||||
|
||||
for i in backend/controller/*; do
|
||||
directories="$$directories,$$i"
|
||||
done
|
||||
|
||||
swag init -d "cmd/server,$$directories" -g server.go
|
||||
|
||||
.PHONY:
|
||||
docker:
|
||||
docker build . -t gitnet.fr/deblan/budget-go
|
||||
|
|
|
|||
|
|
@ -18,12 +18,9 @@ type Controller struct {
|
|||
func New(e *echo.Echo) *Controller {
|
||||
c := Controller{}
|
||||
|
||||
loginRoute := "/login"
|
||||
logoutRoute := "/logout"
|
||||
|
||||
e.GET(loginRoute, c.LoginGet)
|
||||
e.POST(loginRoute, c.LoginPost)
|
||||
e.GET(logoutRoute, c.LogoutGet)
|
||||
e.GET("/login", c.LoginGet)
|
||||
e.POST("/login", c.LoginPost)
|
||||
e.GET("/logout", c.LogoutGet)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
|
@ -53,7 +50,6 @@ func (ctrl *Controller) LoginPost(c echo.Context) error {
|
|||
Path: "/",
|
||||
MaxAge: 3600 * 24 * 2,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
}
|
||||
sess.Values["user"] = user.ID
|
||||
sess.Save(c.Request(), c.Response())
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package bank_account
|
|||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
|
@ -30,45 +29,80 @@ func New(e *echo.Echo) *Controller {
|
|||
crud: crud.New(),
|
||||
}
|
||||
|
||||
listRoute := "/api/bank_account"
|
||||
itemRoute := "/api/bank_account/:id"
|
||||
|
||||
e.GET(listRoute, c.List)
|
||||
e.POST(listRoute, c.Create)
|
||||
e.GET(itemRoute, c.Show)
|
||||
e.POST(itemRoute, c.Update)
|
||||
e.DELETE(itemRoute, c.Delete)
|
||||
e.GET("/api/bank_account", c.List)
|
||||
e.POST("/api/bank_account", c.Create)
|
||||
e.GET("/api/bank_account/:id", c.Show)
|
||||
e.POST("/api/bank_account/:id", c.Update)
|
||||
e.DELETE("/api/bank_account/:id", c.Delete)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// @Summary List bank accounts
|
||||
// @Tags bank_account
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} crud.ListData
|
||||
// @Success 403
|
||||
// @Router /api/bank_account [get]
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).List(c)
|
||||
}
|
||||
|
||||
// @Summary Show a bank account
|
||||
// @Tags bank_account
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.BankAccount
|
||||
// @Success 403
|
||||
// @Success 404
|
||||
// @Param id path int true "ID"
|
||||
// @Router /api/bank_account/{id} [get]
|
||||
func (ctrl *Controller) Show(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Show(c)
|
||||
}
|
||||
|
||||
// @Summary Delete a bank account
|
||||
// @Tags bank_account
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Success 403
|
||||
// @Success 400
|
||||
// @Param id path int true "ID"
|
||||
// @Router /api/bank_account/{id} [delete]
|
||||
func (ctrl *Controller) Delete(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Delete(c)
|
||||
}
|
||||
|
||||
// @Summary Create a bank account
|
||||
// @Tags bank_account
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.BankAccount
|
||||
// @Success 403
|
||||
// @Success 400
|
||||
// @Param label body string true "Name"
|
||||
// @Router /api/bank_account [post]
|
||||
func (ctrl *Controller) Create(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
@ -83,12 +117,11 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
|
||||
return item, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (ctrl *Controller) Update(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package category
|
|||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -37,21 +36,18 @@ func New(e *echo.Echo) *Controller {
|
|||
crud: crud.New(),
|
||||
}
|
||||
|
||||
listRoute := "/api/category"
|
||||
itemRoute := "/api/category/:id"
|
||||
|
||||
e.GET(listRoute, c.List)
|
||||
e.POST(listRoute, c.Create)
|
||||
e.GET(itemRoute, c.Show)
|
||||
e.POST(itemRoute, c.Update)
|
||||
e.DELETE(itemRoute, c.Delete)
|
||||
e.GET("/api/category", c.List)
|
||||
e.POST("/api/category", c.Create)
|
||||
e.GET("/api/category/:id", c.Show)
|
||||
e.POST("/api/category/:id", c.Update)
|
||||
e.DELETE("/api/category/:id", c.Delete)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).List(c)
|
||||
|
|
@ -59,7 +55,7 @@ func (ctrl *Controller) List(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Show(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Show(c)
|
||||
|
|
@ -67,7 +63,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Delete(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Delete(c)
|
||||
|
|
@ -75,7 +71,7 @@ func (ctrl *Controller) Delete(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Create(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
@ -98,7 +94,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Update(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/backend/view"
|
||||
tpl "gitnet.fr/deblan/budget/backend/view/template/collabora"
|
||||
"gitnet.fr/deblan/budget/config"
|
||||
|
|
@ -106,7 +105,7 @@ func (ctrl *Controller) Info(c echo.Context) error {
|
|||
}
|
||||
|
||||
if file == "" {
|
||||
return c.JSON(404, message.FileNotFound)
|
||||
return c.JSON(404, "File not found")
|
||||
}
|
||||
|
||||
fi, _ := os.Stat(file)
|
||||
|
|
@ -142,7 +141,7 @@ func (ctrl *Controller) WopiContentGet(c echo.Context) error {
|
|||
}
|
||||
|
||||
if file == "" {
|
||||
return c.JSON(404, message.FileNotFound)
|
||||
return c.JSON(404, "File not found")
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(file)
|
||||
|
|
@ -166,7 +165,7 @@ func (ctrl *Controller) WopiContentPost(c echo.Context) error {
|
|||
}
|
||||
|
||||
if file == "" {
|
||||
return c.JSON(404, message.FileNotFound)
|
||||
return c.JSON(404, "File not found")
|
||||
}
|
||||
|
||||
data, _ := ioutil.ReadAll(c.Request().Body)
|
||||
|
|
|
|||
|
|
@ -7,45 +7,26 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/go-playground/locales/fr"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
fr_translations "github.com/go-playground/validator/v10/translations/fr"
|
||||
)
|
||||
|
||||
type UpdateCallback func(*gorm.DB, interface{}, interface{}) (interface{}, error)
|
||||
type CreateCallback func(*gorm.DB, interface{}) (interface{}, error)
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Errors validator.ValidationErrorsTranslations `json:"errors"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
Config Configuration
|
||||
Validator *validator.Validate
|
||||
Trans ut.Translator
|
||||
Config Configuration
|
||||
}
|
||||
|
||||
func New() *Controller {
|
||||
c := Controller{}
|
||||
|
||||
fr := fr.New()
|
||||
uni := ut.New(fr, fr)
|
||||
|
||||
trans, _ := uni.GetTranslator("fr")
|
||||
validate := validator.New()
|
||||
fr_translations.RegisterDefaultTranslations(validate, trans)
|
||||
|
||||
c.Validator = validate
|
||||
c.Trans = trans
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
|
|
@ -54,10 +35,6 @@ func (ctrl *Controller) With(config Configuration) *Controller {
|
|||
return ctrl
|
||||
}
|
||||
|
||||
var (
|
||||
IdEqual = "id = ?"
|
||||
)
|
||||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
db := manager.Get().Db
|
||||
db = db.Model(ctrl.Config.Model)
|
||||
|
|
@ -115,7 +92,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
}
|
||||
|
||||
var count int64
|
||||
db.Model(ctrl.Config.Model).Where(IdEqual, value).Count(&count)
|
||||
db.Model(ctrl.Config.Model).Where("id = ?", value).Count(&count)
|
||||
|
||||
if count == 0 {
|
||||
return c.JSON(404, Error{
|
||||
|
|
@ -125,7 +102,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
}
|
||||
|
||||
item := ctrl.Config.CreateModel()
|
||||
db.Model(ctrl.Config.Model).Where(IdEqual, value)
|
||||
db.Model(ctrl.Config.Model).Where("id = ?", value)
|
||||
|
||||
if ctrl.Config.ItemQuery != nil {
|
||||
ctrl.Config.ItemQuery(db)
|
||||
|
|
@ -148,17 +125,17 @@ func (ctrl *Controller) Delete(c echo.Context) error {
|
|||
}
|
||||
|
||||
var count int64
|
||||
db.Model(ctrl.Config.Model).Where(IdEqual, value).Count(&count)
|
||||
db.Model(ctrl.Config.Model).Where("id = ?", value).Count(&count)
|
||||
|
||||
if count == 0 {
|
||||
return c.JSON(404, Error{
|
||||
Code: 404,
|
||||
Message: message.NotFound,
|
||||
Message: "Not found",
|
||||
})
|
||||
}
|
||||
|
||||
item := ctrl.Config.CreateModel()
|
||||
db.Model(ctrl.Config.Model).Where(IdEqual, value).Delete(&item)
|
||||
db.Model(ctrl.Config.Model).Where("id = ?", value).Delete(&item)
|
||||
|
||||
return c.JSON(200, nil)
|
||||
}
|
||||
|
|
@ -169,17 +146,14 @@ func (ctrl *Controller) Create(c echo.Context, body interface{}, createCallback
|
|||
if err := c.Bind(body); err != nil {
|
||||
return c.JSON(400, Error{
|
||||
Code: 400,
|
||||
Message: message.BadRequest,
|
||||
Message: "Bad request",
|
||||
})
|
||||
}
|
||||
|
||||
if err := ctrl.Validator.Struct(body); err != nil {
|
||||
errs := err.(validator.ValidationErrors)
|
||||
|
||||
if err := c.Validate(body); err != nil {
|
||||
return c.JSON(400, Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: errs.Translate(ctrl.Trans),
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -207,12 +181,12 @@ func (ctrl *Controller) Update(c echo.Context, body interface{}, updateCallback
|
|||
}
|
||||
|
||||
var count int64
|
||||
db.Model(ctrl.Config.Model).Where(IdEqual, value).Count(&count)
|
||||
db.Model(ctrl.Config.Model).Where("id = ?", value).Count(&count)
|
||||
|
||||
if count == 0 {
|
||||
return c.JSON(404, Error{
|
||||
Code: 404,
|
||||
Message: message.NotFound,
|
||||
Message: "Not found",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -232,13 +206,10 @@ func (ctrl *Controller) Update(c echo.Context, body interface{}, updateCallback
|
|||
})
|
||||
}
|
||||
|
||||
if err := ctrl.Validator.Struct(body); err != nil {
|
||||
errs := err.(validator.ValidationErrors)
|
||||
|
||||
if err := c.Validate(body); err != nil {
|
||||
return c.JSON(400, Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: errs.Translate(ctrl.Trans),
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/config"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
f "gitnet.fr/deblan/budget/file"
|
||||
|
|
@ -32,7 +31,7 @@ func New(e *echo.Echo) *Controller {
|
|||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
tree := f.GetTree("", config.Get().File.Path)
|
||||
|
|
@ -42,7 +41,7 @@ func (ctrl *Controller) List(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) CreateFile(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
|
|
@ -78,7 +77,7 @@ func (ctrl *Controller) CreateFile(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) CreateDirectory(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
@ -127,7 +126,7 @@ func (ctrl *Controller) CreateDirectory(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Download(c echo.Context) error {
|
||||
if nil == model.LoadUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
var file string
|
||||
|
|
@ -144,7 +143,7 @@ func (ctrl *Controller) Download(c echo.Context) error {
|
|||
}
|
||||
|
||||
if file == "" {
|
||||
return c.JSON(404, message.FileNotFound)
|
||||
return c.JSON(404, "File not found")
|
||||
}
|
||||
|
||||
content, _ := os.ReadFile(file)
|
||||
|
|
@ -154,7 +153,7 @@ func (ctrl *Controller) Download(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Delete(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
var file string
|
||||
|
|
@ -178,7 +177,7 @@ func (ctrl *Controller) Delete(c echo.Context) error {
|
|||
}
|
||||
|
||||
if file == "" {
|
||||
return c.JSON(404, message.FileNotFound)
|
||||
return c.JSON(404, "File not found")
|
||||
}
|
||||
|
||||
os.RemoveAll(file)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package saving_account
|
|||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
|
@ -30,21 +29,18 @@ func New(e *echo.Echo) *Controller {
|
|||
crud: crud.New(),
|
||||
}
|
||||
|
||||
listRoute := "/api/saving_account"
|
||||
itemRoute := "/api/saving_account/:id"
|
||||
|
||||
e.GET(listRoute, c.List)
|
||||
e.POST(listRoute, c.Create)
|
||||
e.GET(itemRoute, c.Show)
|
||||
e.POST(itemRoute, c.Update)
|
||||
e.DELETE(itemRoute, c.Delete)
|
||||
e.GET("/api/saving_account", c.List)
|
||||
e.POST("/api/saving_account", c.Create)
|
||||
e.GET("/api/saving_account/:id", c.Show)
|
||||
e.POST("/api/saving_account/:id", c.Update)
|
||||
e.DELETE("/api/saving_account/:id", c.Delete)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).List(c)
|
||||
|
|
@ -52,7 +48,7 @@ func (ctrl *Controller) List(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Show(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Show(c)
|
||||
|
|
@ -60,7 +56,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Delete(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Delete(c)
|
||||
|
|
@ -68,7 +64,7 @@ func (ctrl *Controller) Delete(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Create(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
@ -90,7 +86,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Update(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import (
|
|||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -23,7 +21,7 @@ func (ctrl *Controller) Config() crud.Configuration {
|
|||
Table: "transactions",
|
||||
Model: model.Transaction{},
|
||||
Models: []model.Transaction{},
|
||||
ValidOrders: []string{"id", "accounted_at", "short_label", "label", "reference", "information", "operation_type", "debit", "credit", "date", "category_id", "bank_account_id"},
|
||||
ValidOrders: []string{"accounted_at", "short_label", "label", "reference", "information", "operation_type", "debit", "credit", "date", "category_id", "bank_account_id"},
|
||||
DefaultLimit: 20,
|
||||
DefaultOrder: "date",
|
||||
DefaultSort: "desc",
|
||||
|
|
@ -52,7 +50,7 @@ func New(e *echo.Echo) *Controller {
|
|||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).List(c)
|
||||
|
|
@ -60,7 +58,7 @@ func (ctrl *Controller) List(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Show(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Show(c)
|
||||
|
|
@ -68,7 +66,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) UpdateCategories(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
datas := model.UpdateTransactionsCategories()
|
||||
|
|
@ -78,7 +76,7 @@ func (ctrl *Controller) UpdateCategories(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Create(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
db := manager.Get().Db
|
||||
|
|
@ -87,10 +85,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
if err != nil {
|
||||
return c.JSON(400, crud.Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: validator.ValidationErrorsTranslations{
|
||||
"BankAccount": message.BlankBankAccount,
|
||||
},
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -100,10 +95,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
if count == 0 {
|
||||
return c.JSON(400, crud.Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: validator.ValidationErrorsTranslations{
|
||||
"BankAccount": message.UnknownBankAccount,
|
||||
},
|
||||
Message: "Invalid bank account",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +103,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
if err != nil {
|
||||
return c.JSON(400, crud.Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: validator.ValidationErrorsTranslations{
|
||||
"File": message.BlankFile,
|
||||
},
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -122,10 +111,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
if err != nil {
|
||||
return c.JSON(400, crud.Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: validator.ValidationErrorsTranslations{
|
||||
"File": message.UnproccessableFile,
|
||||
},
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +125,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
if err != nil {
|
||||
return c.JSON(400, crud.Error{
|
||||
Code: 400,
|
||||
Message: message.InvalidForm,
|
||||
Errors: validator.ValidationErrorsTranslations{
|
||||
"File": message.InvalidFile,
|
||||
},
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package user
|
|||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/budget/backend/controller/crud"
|
||||
"gitnet.fr/deblan/budget/backend/message"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
|
@ -29,21 +28,18 @@ func New(e *echo.Echo) *Controller {
|
|||
crud: crud.New(),
|
||||
}
|
||||
|
||||
listRoute := "/api/user"
|
||||
itemRoute := "/api/user/:id"
|
||||
|
||||
e.GET(listRoute, c.List)
|
||||
e.POST(listRoute, c.Create)
|
||||
e.GET(itemRoute, c.Show)
|
||||
e.POST(itemRoute, c.Update)
|
||||
e.DELETE(itemRoute, c.Delete)
|
||||
e.GET("/api/user", c.List)
|
||||
e.POST("/api/user", c.Create)
|
||||
e.GET("/api/user/:id", c.Show)
|
||||
e.POST("/api/user/:id", c.Update)
|
||||
e.DELETE("/api/user/:id", c.Delete)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (ctrl *Controller) List(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).List(c)
|
||||
|
|
@ -51,7 +47,7 @@ func (ctrl *Controller) List(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Show(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Show(c)
|
||||
|
|
@ -59,7 +55,7 @@ func (ctrl *Controller) Show(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Delete(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
return ctrl.crud.With(ctrl.Config()).Delete(c)
|
||||
|
|
@ -67,7 +63,7 @@ func (ctrl *Controller) Delete(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Create(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
@ -88,7 +84,7 @@ func (ctrl *Controller) Create(c echo.Context) error {
|
|||
|
||||
func (ctrl *Controller) Update(c echo.Context) error {
|
||||
if nil == model.LoadApiUser(c) {
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: message.LoginRequired})
|
||||
return c.JSON(403, crud.Error{Code: 403, Message: "Login required"})
|
||||
}
|
||||
|
||||
type body struct {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package message
|
||||
|
||||
var (
|
||||
LoginRequired = "Login required"
|
||||
FileNotFound = "File not found"
|
||||
NotFound = "Not found"
|
||||
BadRequest = "Bad request"
|
||||
InvalidForm = "Le formulaire n'est pas valide"
|
||||
BlankBankAccount = "Le compte bancaire n'est pas renseigné"
|
||||
UnknownBankAccount = "Le compte bancaire n'existe pas"
|
||||
BlankFile = "Le fichier n'est pas renseigné"
|
||||
UnproccessableFile = "Le fichier ne peut être traité"
|
||||
InvalidFile = "Le fichier n'est pas valide"
|
||||
)
|
||||
|
|
@ -11,9 +11,9 @@ templ Page(hasError bool) {
|
|||
<div class="container py-5 h-100">
|
||||
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card" style="border-radius: 1rem;">
|
||||
<div class="card text-white" style="border-radius: 1rem;">
|
||||
<div class="card-body py-2 px-5">
|
||||
<div class="mb-5 mt-4">
|
||||
<div class="mb-md-5 mt-md-4">
|
||||
<div class="text-center pb-4">
|
||||
<span class="fs-4">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func Page(hasError bool) templ.Component {
|
|||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body><section class=\"vh-100\" id=\"login-form\"><div class=\"container py-5 h-100\"><div class=\"row d-flex justify-content-center align-items-center h-100\"><div class=\"col-12 col-md-8 col-lg-6 col-xl-5\"><div class=\"card\" style=\"border-radius: 1rem;\"><div class=\"card-body py-2 px-5\"><div class=\"mb-5 mt-4\"><div class=\"text-center pb-4\"><span class=\"fs-4\"><i class=\"fa-solid fa-coins\"></i> <span id=\"app-name\">Budget</span></span></div><form action=\"/login\" method=\"POST\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body><section class=\"vh-100\" id=\"login-form\"><div class=\"container py-5 h-100\"><div class=\"row d-flex justify-content-center align-items-center h-100\"><div class=\"col-12 col-md-8 col-lg-6 col-xl-5\"><div class=\"card text-white\" style=\"border-radius: 1rem;\"><div class=\"card-body py-2 px-5\"><div class=\"mb-md-5 mt-md-4\"><div class=\"text-center pb-4\"><span class=\"fs-4\"><i class=\"fa-solid fa-coins\"></i> <span id=\"app-name\">Budget</span></span></div><form action=\"/login\" method=\"POST\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
|
|
|||
19
bin/watch.sh
19
bin/watch.sh
|
|
@ -2,12 +2,31 @@
|
|||
|
||||
export TEMPL_EXPERIMENT=rawgo
|
||||
|
||||
title() {
|
||||
printf -- "# %s\n" "$1"
|
||||
}
|
||||
|
||||
while true; do
|
||||
clear
|
||||
|
||||
title "Remove rice-box"
|
||||
rm -f cmd/server/rice-box.go
|
||||
|
||||
title "Build assets"
|
||||
./node_modules/.bin/webpack
|
||||
|
||||
title "Make templates"
|
||||
make tpl
|
||||
|
||||
title "Make swagger"
|
||||
make swagger
|
||||
|
||||
title "Start server"
|
||||
screen -S budget -d -m go run ./cmd/server
|
||||
|
||||
title "Notify"
|
||||
notify-send "Budget" "Ready!"
|
||||
|
||||
inotifywait -r . -e close_write
|
||||
screen -X -S budget quit
|
||||
done
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ import (
|
|||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
echoSwagger "github.com/swaggo/echo-swagger"
|
||||
|
||||
// _ "github.com/swaggo/echo-swagger/example/docs"
|
||||
"gitnet.fr/deblan/budget/backend/router"
|
||||
"gitnet.fr/deblan/budget/config"
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
"gitnet.fr/deblan/budget/database/model"
|
||||
"gitnet.fr/deblan/budget/backend/router"
|
||||
"gitnet.fr/deblan/budget/docs"
|
||||
)
|
||||
|
||||
type TemplateRenderer struct {
|
||||
|
|
@ -32,7 +36,16 @@ func (cv *AppValidator) Validate(i interface{}) error {
|
|||
return cv.validator.Struct(i)
|
||||
}
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name x-api-key
|
||||
func main() {
|
||||
docs.SwaggerInfo.Title = "Budget API"
|
||||
docs.SwaggerInfo.Description = "API"
|
||||
docs.SwaggerInfo.Version = "1.0"
|
||||
docs.SwaggerInfo.BasePath = "/"
|
||||
docs.SwaggerInfo.Schemes = []string{"http", "https"}
|
||||
|
||||
ini := flag.String("c", "config.ini", "Path to config.ini")
|
||||
conf := config.Get()
|
||||
conf.Load(*ini)
|
||||
|
|
@ -50,6 +63,7 @@ func main() {
|
|||
|
||||
assetHandler := http.FileServer(rice.MustFindBox("../../backend/view/static").HTTPBox())
|
||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
|
||||
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
||||
e.Use(middleware.Logger())
|
||||
router.RegisterControllers(e)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
|
|
@ -57,6 +64,8 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
|
|||
if match {
|
||||
counter += 8
|
||||
}
|
||||
} else {
|
||||
match = match && true
|
||||
}
|
||||
|
||||
if rule.BankCategory != nil {
|
||||
|
|
@ -66,6 +75,8 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
|
|||
if match {
|
||||
counter += 6
|
||||
}
|
||||
} else {
|
||||
match = match && true
|
||||
}
|
||||
|
||||
if rule.Amount != nil {
|
||||
|
|
@ -74,6 +85,8 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
|
|||
if match {
|
||||
counter += 4
|
||||
}
|
||||
} else {
|
||||
match = match && true
|
||||
}
|
||||
|
||||
if rule.DateFrom != nil {
|
||||
|
|
@ -82,6 +95,8 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
|
|||
if match {
|
||||
counter += 2
|
||||
}
|
||||
} else {
|
||||
match = match && true
|
||||
}
|
||||
|
||||
if rule.DateTo != nil {
|
||||
|
|
@ -90,10 +105,8 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
|
|||
if match {
|
||||
counter += 2
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
fmt.Printf("%+v\n", match)
|
||||
} else {
|
||||
match = match && true
|
||||
}
|
||||
|
||||
return match, counter
|
||||
|
|
@ -118,15 +131,149 @@ func (t *Transaction) MatchCategory(category Category) (bool, int) {
|
|||
return match, maxCounter
|
||||
}
|
||||
|
||||
func ImportCaisseEpargneTransactions(content string, bankAccountID int) ([]Transaction, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
db := manager.Get().Db
|
||||
datas := []Transaction{}
|
||||
|
||||
for key, line := range lines {
|
||||
if key == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Split(line, ";")
|
||||
|
||||
if len(fields) != 13 {
|
||||
return datas, errors.New("Invalid format")
|
||||
}
|
||||
|
||||
ref := fields[3]
|
||||
|
||||
if ref == "" {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(line))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
ref = hex.EncodeToString(hashInBytes)
|
||||
}
|
||||
|
||||
var count int64
|
||||
|
||||
db.Model(Transaction{}).Where(Transaction{Reference: ref}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
transaction := Transaction{
|
||||
ShortLabel: toUTF8(fields[1]),
|
||||
Label: toUTF8(fields[2]),
|
||||
Reference: ref,
|
||||
Information: fields[4],
|
||||
OperationType: fields[5],
|
||||
AccountedAt: toDate(fields[0], "02/01/2006"),
|
||||
BankCategory: fields[6],
|
||||
BankSubCategory: fields[7],
|
||||
Debit: -toFloat(fields[8]),
|
||||
Credit: toFloat(fields[9]),
|
||||
BankAccountID: bankAccountID,
|
||||
Date: toDate(fields[10], "02/01/2006"),
|
||||
}
|
||||
|
||||
db.Model(Transaction{}).Save(&transaction)
|
||||
|
||||
datas = append(datas, transaction)
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
func ImportRevolutTransactions(content string, bankAccountID int) ([]Transaction, error) {
|
||||
db := manager.Get().Db
|
||||
datas := []Transaction{}
|
||||
|
||||
r := csv.NewReader(strings.NewReader(content))
|
||||
records, err := r.ReadAll()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, fields := range records {
|
||||
if key == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(fields) != 10 {
|
||||
return datas, errors.New("Invalid format")
|
||||
}
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(strings.Join(fields, ",")))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
ref := hex.EncodeToString(hashInBytes)
|
||||
|
||||
if fields[8] != "COMPLETED" {
|
||||
continue
|
||||
}
|
||||
|
||||
var count int64
|
||||
|
||||
db.Model(Transaction{}).Where(Transaction{Reference: ref}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
amount := toFloat(fields[5])
|
||||
var debit float64
|
||||
var credit float64
|
||||
|
||||
if amount < 0 {
|
||||
debit = -amount
|
||||
credit = 0
|
||||
} else {
|
||||
debit = 0
|
||||
credit = amount
|
||||
}
|
||||
|
||||
date := toDate(fields[2], "2006-01-02 15:04:05")
|
||||
|
||||
transaction := Transaction{
|
||||
ShortLabel: fields[4],
|
||||
Label: fields[4],
|
||||
Reference: ref,
|
||||
Information: "",
|
||||
OperationType: fields[0],
|
||||
AccountedAt: date,
|
||||
BankCategory: "",
|
||||
BankSubCategory: "",
|
||||
Debit: debit,
|
||||
Credit: credit,
|
||||
BankAccountID: bankAccountID,
|
||||
Date: date,
|
||||
}
|
||||
|
||||
db.Model(Transaction{}).Save(&transaction)
|
||||
|
||||
datas = append(datas, transaction)
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
func ImportTransactions(content string, bankAccountID int, format string) ([]Transaction, error) {
|
||||
var datas []Transaction
|
||||
var err error
|
||||
|
||||
if format == "revolut" {
|
||||
datas, err = ImportRevolutTransactions(content, bankAccountID)
|
||||
} else if format == "banque_populaire" {
|
||||
datas, err = ImportBanquePopulaireTransactions(content, bankAccountID)
|
||||
} else if format == "caisse_epargne" {
|
||||
} else {
|
||||
datas, err = ImportCaisseEpargneTransactions(content, bankAccountID)
|
||||
}
|
||||
|
||||
|
|
@ -164,3 +311,23 @@ func UpdateTransactionsCategories() []Transaction {
|
|||
|
||||
return datas
|
||||
}
|
||||
|
||||
func toFloat(value string) float64 {
|
||||
value = strings.ReplaceAll(value, ",", ".")
|
||||
v, _ := strconv.ParseFloat(value, 64)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func toDate(value, format string) time.Time {
|
||||
v, _ := time.Parse(format, value)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func toUTF8(input string) string {
|
||||
reader := transform.NewReader(strings.NewReader(input), charmap.ISO8859_1.NewDecoder())
|
||||
decoded, _ := ioutil.ReadAll(reader)
|
||||
|
||||
return string(decoded)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
)
|
||||
|
||||
func ImportBanquePopulaireTransactions(content string, bankAccountID int) ([]Transaction, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
db := manager.Get().Db
|
||||
datas := []Transaction{}
|
||||
|
||||
for key, line := range lines {
|
||||
if key == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Split(line, ";")
|
||||
|
||||
if len(fields) != 8 {
|
||||
return datas, errors.New("Invalid format")
|
||||
}
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(line))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
ref := hex.EncodeToString(hashInBytes)
|
||||
|
||||
var count int64
|
||||
|
||||
db.Model(Transaction{}).Where(Transaction{
|
||||
Reference: ref,
|
||||
BankAccountID: bankAccountID,
|
||||
}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
amount := ToFloat(fields[6])
|
||||
debit := 0.0
|
||||
credit := 0.0
|
||||
|
||||
if amount > 0 {
|
||||
credit = amount
|
||||
} else {
|
||||
debit = -amount
|
||||
}
|
||||
|
||||
transaction := Transaction{
|
||||
ShortLabel: "",
|
||||
Label: ToUTF8(fields[3]),
|
||||
Reference: ref,
|
||||
Information: "",
|
||||
OperationType: "",
|
||||
AccountedAt: ToDate(fields[1], "02/01/2006"),
|
||||
BankCategory: "",
|
||||
BankSubCategory: "",
|
||||
Debit: debit,
|
||||
Credit: credit,
|
||||
BankAccountID: bankAccountID,
|
||||
Date: ToDate(fields[2], "02/01/2006"),
|
||||
}
|
||||
|
||||
db.Model(Transaction{}).Save(&transaction)
|
||||
|
||||
datas = append(datas, transaction)
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
)
|
||||
|
||||
func ImportCaisseEpargneTransactions(content string, bankAccountID int) ([]Transaction, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
db := manager.Get().Db
|
||||
datas := []Transaction{}
|
||||
|
||||
for key, line := range lines {
|
||||
if key == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Split(line, ";")
|
||||
|
||||
if len(fields) != 13 {
|
||||
return datas, errors.New("Invalid format")
|
||||
}
|
||||
|
||||
ref := fields[3]
|
||||
|
||||
if ref == "" {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(line))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
ref = hex.EncodeToString(hashInBytes)
|
||||
}
|
||||
|
||||
var count int64
|
||||
|
||||
db.Model(Transaction{}).Where(Transaction{
|
||||
Reference: ref,
|
||||
BankAccountID: bankAccountID,
|
||||
}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
transaction := Transaction{
|
||||
ShortLabel: ToUTF8(fields[1]),
|
||||
Label: ToUTF8(fields[2]),
|
||||
Reference: ref,
|
||||
Information: fields[4],
|
||||
OperationType: fields[5],
|
||||
AccountedAt: ToDate(fields[0], "02/01/2006"),
|
||||
BankCategory: fields[6],
|
||||
BankSubCategory: fields[7],
|
||||
Debit: -ToFloat(fields[8]),
|
||||
Credit: ToFloat(fields[9]),
|
||||
BankAccountID: bankAccountID,
|
||||
Date: ToDate(fields[10], "02/01/2006"),
|
||||
}
|
||||
|
||||
db.Model(Transaction{}).Save(&transaction)
|
||||
|
||||
datas = append(datas, transaction)
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func ToFloat(value string) float64 {
|
||||
value = strings.ReplaceAll(value, ",", ".")
|
||||
v, _ := strconv.ParseFloat(value, 64)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func ToDate(value, format string) time.Time {
|
||||
v, _ := time.Parse(format, value)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func ToUTF8(input string) string {
|
||||
reader := transform.NewReader(strings.NewReader(input), charmap.ISO8859_1.NewDecoder())
|
||||
decoded, _ := ioutil.ReadAll(reader)
|
||||
|
||||
return string(decoded)
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gitnet.fr/deblan/budget/database/manager"
|
||||
)
|
||||
|
||||
func ImportRevolutTransactions(content string, bankAccountID int) ([]Transaction, error) {
|
||||
db := manager.Get().Db
|
||||
datas := []Transaction{}
|
||||
|
||||
r := csv.NewReader(strings.NewReader(content))
|
||||
records, err := r.ReadAll()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, fields := range records {
|
||||
if key == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(fields) != 10 {
|
||||
return datas, errors.New("Invalid format")
|
||||
}
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(strings.Join(fields, ",")))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
ref := hex.EncodeToString(hashInBytes)
|
||||
|
||||
if fields[8] != "COMPLETED" {
|
||||
continue
|
||||
}
|
||||
|
||||
var count int64
|
||||
|
||||
db.Model(Transaction{}).Where(Transaction{
|
||||
Reference: ref,
|
||||
BankAccountID: bankAccountID,
|
||||
}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
amount := ToFloat(fields[5])
|
||||
var debit float64
|
||||
var credit float64
|
||||
|
||||
if amount < 0 {
|
||||
debit = -amount
|
||||
credit = 0
|
||||
} else {
|
||||
debit = 0
|
||||
credit = amount
|
||||
}
|
||||
|
||||
date := ToDate(fields[2], "2006-01-02 15:04:05")
|
||||
|
||||
transaction := Transaction{
|
||||
ShortLabel: fields[4],
|
||||
Label: fields[4],
|
||||
Reference: ref,
|
||||
Information: "",
|
||||
OperationType: fields[0],
|
||||
AccountedAt: date,
|
||||
BankCategory: "",
|
||||
BankSubCategory: "",
|
||||
Debit: debit,
|
||||
Credit: credit,
|
||||
BankAccountID: bankAccountID,
|
||||
Date: date,
|
||||
}
|
||||
|
||||
db.Model(Transaction{}).Save(&transaction)
|
||||
|
||||
datas = append(datas, transaction)
|
||||
}
|
||||
|
||||
return datas, nil
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { BNavItem } from 'bootstrap-vue-next'
|
||||
import {RouterLink, RouterView} from 'vue-router'
|
||||
import {BNavItem} from 'bootstrap-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,7 +11,7 @@ import { BNavItem } from 'bootstrap-vue-next'
|
|||
>
|
||||
<a
|
||||
href="/"
|
||||
class="mb-4 text-decoration-none text-center"
|
||||
class="mb-4 text-white text-decoration-none text-center"
|
||||
>
|
||||
<span class="fs-4">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
|
|
@ -20,21 +20,29 @@ import { BNavItem } from 'bootstrap-vue-next'
|
|||
</a>
|
||||
<ul class="nav nav-pills flex-column mb-auto">
|
||||
<RouterLink
|
||||
v-for="url in ['/', '/transactions', '/categories', '/bank_accounts', '/saving_accounts', '/files', '/users']"
|
||||
v-slot="{ href, route, isActive }"
|
||||
:key="url"
|
||||
v-for="url in [
|
||||
'/',
|
||||
'/transactions',
|
||||
'/categories',
|
||||
'/bank_accounts',
|
||||
'/saving_accounts',
|
||||
'/files',
|
||||
'users',
|
||||
]"
|
||||
v-slot="{href, route, navigate, isActive, isExactActive}"
|
||||
:to="url"
|
||||
custom
|
||||
>
|
||||
<BNavItem
|
||||
:href="href"
|
||||
:active="isActive"
|
||||
:link-class="{'text-white': !isActive}"
|
||||
>
|
||||
<i
|
||||
class="me-2"
|
||||
:class="route.meta.icon"
|
||||
></i>
|
||||
<span class="nav-item-label">{{ route.meta.label }}</span>
|
||||
<span class="nav-item-label">{{ route.name }}</span>
|
||||
</BNavItem>
|
||||
</RouterLink>
|
||||
</ul>
|
||||
|
|
@ -42,10 +50,12 @@ import { BNavItem } from 'bootstrap-vue-next'
|
|||
href="/logout"
|
||||
class="px-3 p-lg-0 text-decoration-none"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-right-from-bracket"></i>
|
||||
<span class="ps-2 d-none d-lg-inline">Déconnexion</span>
|
||||
<i class="text-white fa-solid fa-arrow-right-from-bracket"></i>
|
||||
<span class="text-white ps-2 d-none d-lg-inline">Déconnexion</span>
|
||||
</a>
|
||||
</div>
|
||||
<RouterView id="body" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isInRange } from '../lib/dateFilter'
|
||||
import {isInRange} from '../lib/dateFilter'
|
||||
|
||||
const getDate = (value, precision) => {
|
||||
const d = new Date(value)
|
||||
|
|
@ -50,7 +50,7 @@ const compute = (transactions, precision, dateFrom, dateTo) => {
|
|||
|
||||
transactions.forEach((transaction) => {
|
||||
let date = getDate(transaction.date, precision)
|
||||
let begin = !Object.hasOwn(indexes, date)
|
||||
let begin = !Object.prototype.hasOwnProperty.call(indexes, date)
|
||||
|
||||
if (begin) {
|
||||
indexes[date] = labels.length
|
||||
|
|
@ -79,4 +79,4 @@ const compute = (transactions, precision, dateFrom, dateTo) => {
|
|||
return config
|
||||
}
|
||||
|
||||
export { compute }
|
||||
export {compute}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isInRange } from '../lib/dateFilter'
|
||||
import {isInRange} from '../lib/dateFilter'
|
||||
|
||||
const getDate = (value) => {
|
||||
const d = new Date(value)
|
||||
|
|
@ -36,7 +36,7 @@ const compute = (transactions, dateFrom, dateTo, order) => {
|
|||
|
||||
let category = transaction.category ?? emptyCategory
|
||||
|
||||
if (!Object.hasOwn(data, category.label)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, category.label)) {
|
||||
data[category.label] = {
|
||||
category: category,
|
||||
average: 0,
|
||||
|
|
@ -52,7 +52,7 @@ const compute = (transactions, dateFrom, dateTo, order) => {
|
|||
|
||||
const date = getDate(transaction.date)
|
||||
|
||||
if (!Object.hasOwn(data[category.label].months, date)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data[category.label].months, date)) {
|
||||
data[category.label].months[date] = 0
|
||||
}
|
||||
|
||||
|
|
@ -82,4 +82,4 @@ const compute = (transactions, dateFrom, dateTo, order) => {
|
|||
return data
|
||||
}
|
||||
|
||||
export { compute }
|
||||
export {compute}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isInRange } from '../lib/dateFilter'
|
||||
import {isInRange} from '../lib/dateFilter'
|
||||
|
||||
const getDate = (value) => {
|
||||
const d = new Date(value)
|
||||
|
|
@ -39,7 +39,7 @@ const compute = (transactions, dateFrom, dateTo) => {
|
|||
|
||||
const date = getDate(transaction.date)
|
||||
|
||||
if (!Object.hasOwn(indexes, date)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(indexes, date)) {
|
||||
indexes[date] = labels.length
|
||||
values[date] = 0
|
||||
labels.push(date)
|
||||
|
|
@ -68,4 +68,4 @@ const compute = (transactions, dateFrom, dateTo) => {
|
|||
return config
|
||||
}
|
||||
|
||||
export { compute }
|
||||
export {compute}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isInRange } from '../lib/dateFilter'
|
||||
import {isInRange} from '../lib/dateFilter'
|
||||
|
||||
const getDate = (value) => {
|
||||
const d = new Date(value)
|
||||
|
|
@ -13,7 +13,13 @@ const getDate = (value) => {
|
|||
return `${month}/${year}`
|
||||
}
|
||||
|
||||
const computeBar = (transactions, stacked, dateFrom, dateTo, selectedCategories) => {
|
||||
const computeBar = (
|
||||
transactions,
|
||||
stacked,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCategories,
|
||||
) => {
|
||||
const indexes = {}
|
||||
const labels = []
|
||||
const bars = {}
|
||||
|
|
@ -36,12 +42,12 @@ const computeBar = (transactions, stacked, dateFrom, dateTo, selectedCategories)
|
|||
|
||||
let date = getDate(transaction.date)
|
||||
|
||||
if (!Object.hasOwn(indexes, date)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(indexes, date)) {
|
||||
indexes[date] = labels.length
|
||||
labels.push(date)
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(bars, category.label)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(bars, category.label)) {
|
||||
bars[category.label] = {
|
||||
label: category.label,
|
||||
data: [],
|
||||
|
|
@ -72,7 +78,8 @@ const computeBar = (transactions, stacked, dateFrom, dateTo, selectedCategories)
|
|||
|
||||
let date = getDate(transaction.date)
|
||||
|
||||
bars[category.label].data[indexes[date]] += transaction.debit - transaction.credit
|
||||
bars[category.label].data[indexes[date]] +=
|
||||
transaction.debit - transaction.credit
|
||||
})
|
||||
|
||||
labels.forEach((label, key) => {
|
||||
|
|
@ -103,7 +110,12 @@ const computeBar = (transactions, stacked, dateFrom, dateTo, selectedCategories)
|
|||
}
|
||||
}
|
||||
|
||||
const computeDoughnut = (transactions, dateFrom, dateTo, selectedCategories) => {
|
||||
const computeDoughnut = (
|
||||
transactions,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
selectedCategories,
|
||||
) => {
|
||||
const indexes = {}
|
||||
const labels = []
|
||||
const data = []
|
||||
|
|
@ -125,7 +137,7 @@ const computeDoughnut = (transactions, dateFrom, dateTo, selectedCategories) =>
|
|||
return
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(indexes, category.id)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(indexes, category.id)) {
|
||||
indexes[category.id] = labels.length
|
||||
labels.push(category.label)
|
||||
backgroundColor.push(category.color)
|
||||
|
|
@ -152,4 +164,4 @@ const computeDoughnut = (transactions, dateFrom, dateTo, selectedCategories) =>
|
|||
}
|
||||
}
|
||||
|
||||
export { computeDoughnut, computeBar }
|
||||
export {computeDoughnut, computeBar}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isInRange } from '../lib/dateFilter'
|
||||
import {isInRange} from '../lib/dateFilter'
|
||||
|
||||
const getDate = (value) => {
|
||||
const d = new Date(value)
|
||||
|
|
@ -24,7 +24,10 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
|
|||
})
|
||||
|
||||
transactions.forEach((value) => {
|
||||
if (value.category === null || !Object.hasOwn(categories, value.category.id)) {
|
||||
if (
|
||||
value.category === null ||
|
||||
!Object.prototype.hasOwnProperty.call(categories, value.category.id)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -34,11 +37,16 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
|
|||
|
||||
const date = getDate(value.date)
|
||||
|
||||
if (!Object.hasOwn(datas, date)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(datas, date)) {
|
||||
datas[date] = {}
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(datas[date], value.category.id.toString())) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
datas[date],
|
||||
value.category.id.toString(),
|
||||
)
|
||||
) {
|
||||
datas[date][value.category.id.toString()] = 0
|
||||
}
|
||||
|
||||
|
|
@ -65,4 +73,4 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
|
|||
return datas
|
||||
}
|
||||
|
||||
export { compute }
|
||||
export {compute}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ const compute = (accounts) => {
|
|||
}
|
||||
}
|
||||
|
||||
export { compute }
|
||||
export {compute}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
</div>
|
||||
<div class="col-12 col-md-7 mb-1">
|
||||
<BFormInput
|
||||
v-if="item.type == 'string' && needValue(dataValue[key].comparator)"
|
||||
v-if="
|
||||
item.type == 'string' && needValue(dataValue[key].comparator)
|
||||
"
|
||||
v-model="dataValue[key].value"
|
||||
@change="changed"
|
||||
/>
|
||||
|
|
@ -31,14 +33,18 @@
|
|||
/>
|
||||
|
||||
<BFormInput
|
||||
v-if="item.type == 'number' && needValue(dataValue[key].comparator)"
|
||||
v-if="
|
||||
item.type == 'number' && needValue(dataValue[key].comparator)
|
||||
"
|
||||
v-model="dataValue[key].value"
|
||||
type="number"
|
||||
@change="changed"
|
||||
/>
|
||||
|
||||
<BFormSelect
|
||||
v-if="item.type == 'select' && needValue(dataValue[key].comparator)"
|
||||
v-if="
|
||||
item.type == 'select' && needValue(dataValue[key].comparator)
|
||||
"
|
||||
v-model="dataValue[key].value"
|
||||
:options="item.options"
|
||||
@change="changed"
|
||||
|
|
@ -80,34 +86,24 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { BFormGroup, BFormInput, BFormSelect, BButton } from 'bootstrap-vue-next'
|
||||
import {ref, onMounted, defineEmits} from 'vue'
|
||||
import {BFormGroup, BFormInput, BFormSelect, BButton} from 'bootstrap-vue-next'
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: [Array, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps(['data', 'fields'])
|
||||
const dataValue = ref(props.data.value)
|
||||
const choices = ref([])
|
||||
const choice = ref(null)
|
||||
|
||||
const _ = { value: null, text: null }
|
||||
const eq = { value: 'eq', text: '=' }
|
||||
const neq = { value: 'neq', text: '!=' }
|
||||
const like = { value: 'like', text: 'contient' }
|
||||
const nlike = { value: 'nlike', text: 'ne content pas' }
|
||||
const empty = { value: 'empty', text: 'est vide' }
|
||||
const nEmpty = { value: 'nempty', text: 'est pas vide' }
|
||||
const gt = { value: 'gt', text: '>=' }
|
||||
const lt = { value: 'lt', text: '<=' }
|
||||
const _ = {value: null, text: null}
|
||||
const eq = {value: 'eq', text: '='}
|
||||
const neq = {value: 'neq', text: '!='}
|
||||
const like = {value: 'like', text: 'contient'}
|
||||
const nlike = {value: 'nlike', text: 'ne content pas'}
|
||||
const empty = {value: 'empty', text: 'est vide'}
|
||||
const nEmpty = {value: 'nempty', text: 'est pas vide'}
|
||||
const gt = {value: 'gt', text: '>='}
|
||||
const lt = {value: 'lt', text: '<='}
|
||||
|
||||
const stringOptions = [_, eq, neq, like, nlike, empty, nEmpty]
|
||||
const dateOptions = [_, eq, neq, gt, lt, empty, nEmpty]
|
||||
|
|
@ -158,7 +154,7 @@ const doAdd = () => {
|
|||
}
|
||||
|
||||
const doRemove = (key) => {
|
||||
let items = []
|
||||
var items = []
|
||||
|
||||
dataValue.value.forEach((v, k) => {
|
||||
if (k !== key) {
|
||||
|
|
@ -178,7 +174,7 @@ onMounted(() => {
|
|||
dataValue.value = props.data
|
||||
|
||||
for (let field of props.fields) {
|
||||
choices.value.push({ value: field.key, text: field.label })
|
||||
choices.value.push({value: field.key, text: field.label})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<span>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-html="props.label"></span>
|
||||
|
||||
<span
|
||||
|
|
@ -20,28 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
currentOrder: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
currentSort: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
order: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
sort: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const props = defineProps(['currentOrder', 'currentSort', 'order', 'label'])
|
||||
|
||||
const isActive = () => {
|
||||
return props.currentOrder === props.order
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<template>
|
||||
<div class="header d-block p-3 mb-3">
|
||||
<div class="d-md-flex justify-content-between menu">
|
||||
<h3>{{ title }}</h3>
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.bottom"
|
||||
class="pt-3"
|
||||
>
|
||||
<slot name="bottom"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.menu .btn {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.menu .btn:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<template>
|
||||
<BTableSimple caption-top>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="header in headers"
|
||||
:key="header.key"
|
||||
:width="header.width"
|
||||
class="cursor"
|
||||
:class="header.thClasses"
|
||||
valign="top"
|
||||
@click="doSort(header.orderKey ?? header.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="header.orderKey ?? header.key"
|
||||
:label="header.label ?? header.renderLabel(rows)"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="(row, key) in rows"
|
||||
:key="key"
|
||||
>
|
||||
<BTd
|
||||
v-for="header in headers"
|
||||
:key="header.key"
|
||||
class="cursor"
|
||||
:class="header.tdClasses"
|
||||
@click="rowClick(row)"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-if="header.key">
|
||||
<span
|
||||
v-if="header.render"
|
||||
v-html="header.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[header.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="header.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BTableSimple, BThead, BTbody, BTr, BTh, BTd } from 'bootstrap-vue-next'
|
||||
import SortButton from '../SortButton.vue'
|
||||
|
||||
defineProps({
|
||||
headers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
order: {
|
||||
type: [String, null],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
sort: {
|
||||
type: [String, null],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['sort', 'rowClick'])
|
||||
|
||||
const doSort = (key) => {
|
||||
emit('sort', key)
|
||||
}
|
||||
|
||||
const rowClick = (row) => {
|
||||
emit('rowClick', row)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<template
|
||||
v-for="(field, key) in form.fields"
|
||||
:key="key"
|
||||
>
|
||||
<FormWidget
|
||||
v-if="fields.length === 0 || fields.includes(field.key)"
|
||||
:form="form"
|
||||
:field="field"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FormWidget from './FormWidget.vue'
|
||||
|
||||
defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
<template>
|
||||
<BFormGroup
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<BFormFile
|
||||
v-if="(field.widget ?? 'text') === 'file'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<BFormSelect
|
||||
v-if="(field.widget ?? 'text') === 'select'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:options="field.options"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="field.widget === 'rules'"
|
||||
id="rules"
|
||||
>
|
||||
<div v-if="form.data[field.key] !== null && form.data[field.key].length > 0">
|
||||
<div
|
||||
v-for="(rule, key) in form.data[field.key]"
|
||||
:key="key"
|
||||
class="p-3"
|
||||
:class="{ 'border-bottom': key + 1 !== form.data[field.key].length }"
|
||||
>
|
||||
<div class="d-block d-lg-flex justify-content-between gap-1">
|
||||
<BFormInput
|
||||
v-if="form.data[field.key][key].id !== null"
|
||||
v-model="form.data[field.key][key].id"
|
||||
type="hidden"
|
||||
/>
|
||||
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Libellé contient"
|
||||
:label-for="'form-rule-contain-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-contain-' + key"
|
||||
v-model="form.data[field.key][key].contain"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Regex libellé"
|
||||
:label-for="'form-rule-match-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-match-' + key"
|
||||
v-model="form.data[field.key][key].match"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Catégorie banque"
|
||||
:label-for="'form-rule-contain-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-bank-category-contain-' + key"
|
||||
v-model="form.data[field.key][key].bank_category"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Montant"
|
||||
:label-for="'form-rule-amount-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-amount-' + key"
|
||||
v-model="form.data[field.key][key].amount"
|
||||
type="number"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Du"
|
||||
:label-for="'form-rule-datefrom-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-datefrom-' + key"
|
||||
v-model="form.data[field.key][key].date_from"
|
||||
type="date"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Au"
|
||||
:label-for="'form-rule-dateto-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-dateto-' + key"
|
||||
v-model="form.data[field.key][key].date_to"
|
||||
type="date"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BButton
|
||||
variant="none"
|
||||
@click="form.data[field.key] = doRemoveRule(form.data[field.key], key)"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BFormGroup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BFormGroup, BFormSelect, BFormInput, BFormFile, BButton } from 'bootstrap-vue-next'
|
||||
|
||||
defineProps({
|
||||
key: {
|
||||
required: true,
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const doRemoveRule = (item, key) => {
|
||||
let values = []
|
||||
|
||||
item.forEach((v, k) => {
|
||||
if (k !== key) {
|
||||
values.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
return values
|
||||
}
|
||||
</script>
|
||||
12
frontend/js/components/crud/Header.vue
Normal file
12
frontend/js/components/crud/Header.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="d-block d-md-flex justify-content-between p-3">
|
||||
<h3>{{ title }}</h3>
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps} from 'vue'
|
||||
|
||||
defineProps(['title'])
|
||||
</script>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<div class="d-flex justify-content-end gap-2 pe-3 pb-2">
|
||||
<BPagination
|
||||
v-if="pages > 1"
|
||||
:model-value="page"
|
||||
:per-page="limit"
|
||||
:total-rows="limit * pages"
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
:model-value="limit"
|
||||
:options="limits()"
|
||||
:required="true"
|
||||
style="width: 80px"
|
||||
style="width: 70px"
|
||||
size="sm"
|
||||
@change="updateLimit($event.target.value)"
|
||||
/>
|
||||
|
|
@ -21,22 +20,10 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BFormSelect, BPagination } from 'bootstrap-vue-next'
|
||||
import {defineProps, defineEmits} from 'vue'
|
||||
import {BFormSelect, BPagination} from 'bootstrap-vue-next'
|
||||
|
||||
defineProps({
|
||||
page: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
pages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['page', 'pages', 'limit'])
|
||||
|
||||
const emit = defineEmits(['update:page', 'update:limit', 'update'])
|
||||
|
||||
|
|
@ -56,7 +43,7 @@ const limits = () => {
|
|||
|
||||
for (let i of [10, 20, 50, 100, 0]) {
|
||||
let label = i !== 0 ? i : 'Tout'
|
||||
v.push({ value: i, text: label })
|
||||
v.push({value: i, text: label})
|
||||
}
|
||||
|
||||
return v
|
||||
|
|
@ -21,27 +21,14 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { compute } from '../../chart/capital'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import { BFormSelect } from 'bootstrap-vue-next'
|
||||
import { chartStyle } from '../../lib/chartStyle'
|
||||
import {ref, watch, defineProps} from 'vue'
|
||||
import {getStorage, saveStorage} from '../../lib/storage'
|
||||
import {compute} from '../../chart/capital'
|
||||
import {Line} from 'vue-chartjs'
|
||||
import {BFormSelect} from 'bootstrap-vue-next'
|
||||
import {chartStyle} from '../../lib/chartStyle'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
dateTo: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['data', 'dateFrom', 'dateTo'])
|
||||
|
||||
const precision = ref(getStorage('dashboard:capital:precision'))
|
||||
|
||||
|
|
@ -49,9 +36,9 @@ watch(precision, (v) => saveStorage('dashboard:capital:precision', v))
|
|||
|
||||
const precisions = () => {
|
||||
return [
|
||||
{ value: 'month', text: 'Mois' },
|
||||
{ value: '2weeks', text: '2 semaines' },
|
||||
{ value: 'day', text: 'Jour' },
|
||||
{value: 'month', text: 'Mois'},
|
||||
{value: '2weeks', text: '2 semaines'},
|
||||
{value: 'day', text: 'Jour'},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -19,10 +19,8 @@
|
|||
<BTh
|
||||
width="170px"
|
||||
class="text-end"
|
||||
v-html="renderLabelWithSum('Total', computed(data, dateFrom, dateTo, order), 'sum')"></BTh
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-html="renderLabelWithSum('Total', computed(data, dateFrom, dateTo, order), 'sum')"></span>
|
||||
</BTh>
|
||||
<BTh
|
||||
width="250px"
|
||||
class="text-end"
|
||||
|
|
@ -48,10 +46,10 @@
|
|||
v-for="item in computed(data, dateFrom, dateTo, order)"
|
||||
:key="item.category.id"
|
||||
>
|
||||
<BTd class="text-start">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-html="renderCategory(item.category)"></span>
|
||||
</BTd>
|
||||
<BTd
|
||||
class="text-start"
|
||||
v-html="renderCategory(item.category)"
|
||||
></BTd>
|
||||
<BTd class="text-end">{{ renderEuro(item.sum) }}</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(item.average) }}</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(item.monthAverage) }}</BTd>
|
||||
|
|
@ -69,18 +67,30 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { BFormSelect, BTableSimple, BThead, BTbody, BTr, BTh, BTd } from 'bootstrap-vue-next'
|
||||
import { compute } from '../../chart/debitAverage'
|
||||
import { renderLabelWithSum, renderCategory, renderEuro } from '../../lib/renderers'
|
||||
import {ref, watch} from 'vue'
|
||||
import {getStorage, saveStorage} from '../../lib/storage'
|
||||
import {
|
||||
BFormSelect,
|
||||
BTableSimple,
|
||||
BThead,
|
||||
BTbody,
|
||||
BTr,
|
||||
BTh,
|
||||
BTd,
|
||||
} from 'bootstrap-vue-next'
|
||||
import {compute} from '../../chart/debitAverage'
|
||||
import {
|
||||
renderLabelWithSum,
|
||||
renderCategory,
|
||||
renderEuro
|
||||
} from '../../lib/renderers'
|
||||
|
||||
const order = ref(getStorage('dashboard:debitAverage:order', 'sum'))
|
||||
const orders = [
|
||||
{ value: 'sum', text: 'Total' },
|
||||
{ value: 'average', text: 'Débit moyen / transaction' },
|
||||
{ value: 'monthAverage', text: 'Débit moyen / mois' },
|
||||
{ value: 'count', text: 'Transactions' },
|
||||
{value: 'sum', text: 'Total'},
|
||||
{value: 'average', text: 'Débit moyen / transaction'},
|
||||
{value: 'monthAverage', text: 'Débit moyen / mois'},
|
||||
{value: 'count', text: 'Transactions'},
|
||||
]
|
||||
|
||||
let cache = null
|
||||
|
|
@ -101,20 +111,7 @@ const computed = (data, dateFrom, dateTo, order) => {
|
|||
|
||||
watch(order, (v) => saveStorage('dashboard:debitAverage:order', v))
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
dateTo: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['data', 'dateFrom', 'dateTo'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -13,24 +13,11 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import { compute } from '../../chart/diffCreditDebit'
|
||||
import { chartStyle } from '../../lib/chartStyle'
|
||||
import {Bar} from 'vue-chartjs'
|
||||
import {compute} from '../../chart/diffCreditDebit'
|
||||
import {chartStyle} from '../../lib/chartStyle'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
dateTo: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['data', 'dateFrom', 'dateTo'])
|
||||
|
||||
const options = () => {
|
||||
return {
|
||||
|
|
@ -7,10 +7,9 @@
|
|||
v-for="(item, key) in categories"
|
||||
:key="key"
|
||||
class="col-auto checkbox"
|
||||
:class="{ 'checkbox--unchecked': !selectedCategories[item.label] }"
|
||||
:class="{'checkbox--unchecked': !selectedCategories[item.label]}"
|
||||
>
|
||||
<BFormCheckbox v-model="selectedCategories[item.label]">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="cursor"
|
||||
v-html="renderCategory(item)"
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
<div class="col-12 col-lg-4">
|
||||
<Doughnut
|
||||
:data="computeDoughnut(data, dateFrom, dateTo, selectedCategories)"
|
||||
:options="fixedOptions({ plugins: { legend: { display: false } } })"
|
||||
:options="fixedOptions({plugins: {legend: {display: false}}})"
|
||||
:style="chartStyle(380)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -53,8 +52,10 @@
|
|||
</div>
|
||||
<div>
|
||||
<Bar
|
||||
:data="computeBar(data, isStacked, dateFrom, dateTo, selectedCategories)"
|
||||
:options="stackOptions({ plugins: { legend: { display: false } } })"
|
||||
:data="
|
||||
computeBar(data, isStacked, dateFrom, dateTo, selectedCategories)
|
||||
"
|
||||
:options="stackOptions({plugins: {legend: {display: false}}})"
|
||||
:style="chartStyle(380)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -64,36 +65,19 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, toRefs, watch, onMounted } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { computeDoughnut, computeBar } from '../../chart/distribution'
|
||||
import { renderCategory } from '../../lib/renderers'
|
||||
import { Doughnut, Bar } from 'vue-chartjs'
|
||||
import { BFormCheckbox } from 'bootstrap-vue-next'
|
||||
import { chartStyle } from '../../lib/chartStyle'
|
||||
import {ref, toRefs, watch, defineProps, onMounted} from 'vue'
|
||||
import {getStorage, saveStorage} from '../../lib/storage'
|
||||
import {computeDoughnut, computeBar} from '../../chart/distribution'
|
||||
import {renderCategory} from '../../lib/renderers'
|
||||
import {Doughnut, Bar} from 'vue-chartjs'
|
||||
import {BFormCheckbox} from 'bootstrap-vue-next'
|
||||
import {chartStyle} from '../../lib/chartStyle'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
dateTo: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const props = defineProps(['data', 'categories', 'dateFrom', 'dateTo'])
|
||||
|
||||
const isStacked = ref(getStorage('dashboard:distribution:isStacked'))
|
||||
const selectedCategories = ref({})
|
||||
const { categories } = toRefs(props)
|
||||
const {categories} = toRefs(props)
|
||||
|
||||
watch(isStacked, (v) => saveStorage('dashboard:distribution:isStacked', v))
|
||||
|
||||
|
|
@ -108,11 +92,11 @@ const fixedOptions = (opts) => {
|
|||
}
|
||||
|
||||
const stackOptions = (opts) => {
|
||||
let options = { ...fixedOptions(opts) }
|
||||
let options = {...fixedOptions(opts)}
|
||||
|
||||
options.scales = {
|
||||
y: { stacked: true, ticks: { beginAtZero: true } },
|
||||
x: { stacked: true },
|
||||
y: {stacked: true, ticks: {beginAtZero: true}},
|
||||
x: {stacked: true},
|
||||
}
|
||||
|
||||
return options
|
||||
|
|
@ -63,31 +63,19 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toRefs } from 'vue'
|
||||
import { BFormInput } from 'bootstrap-vue-next'
|
||||
import {toRefs, defineProps, defineEmits} from 'vue'
|
||||
import {BFormInput} from 'bootstrap-vue-next'
|
||||
|
||||
const emit = defineEmits(['update:account', 'update:dateFrom', 'update:dateTo', 'update'])
|
||||
const emit = defineEmits([
|
||||
'update:account',
|
||||
'update:dateFrom',
|
||||
'update:dateTo',
|
||||
'update',
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
account: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
accounts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
dateFrom: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
dateTo: {
|
||||
type: [String, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const props = defineProps(['account', 'accounts', 'dateFrom', 'dateTo'])
|
||||
|
||||
const { dateFrom, dateTo } = toRefs(props)
|
||||
const {dateFrom, dateTo} = toRefs(props)
|
||||
|
||||
const change = (event, value) => {
|
||||
emit(event, value)
|
||||
|
|
@ -33,18 +33,19 @@
|
|||
</BThead>
|
||||
<BTbody>
|
||||
<template v-for="(item, date) in data">
|
||||
<BTr
|
||||
v-for="(row, key) in item"
|
||||
:key="key"
|
||||
>
|
||||
<BTr v-for="data in item">
|
||||
<BTd class="text-start">{{ date }}</BTd>
|
||||
<BTd class="text-start">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span v-html="renderCategory(row.category)"></span>
|
||||
</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(row.category.month_threshold) }}</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(row.amount) }}</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(row.amount - row.category.month_threshold) }}</BTd>
|
||||
<BTd
|
||||
class="text-start"
|
||||
v-html="renderCategory(data.category)"
|
||||
></BTd>
|
||||
<BTd class="text-end">{{
|
||||
renderEuro(data.category.month_threshold)
|
||||
}}</BTd>
|
||||
<BTd class="text-end">{{ renderEuro(data.amount) }}</BTd>
|
||||
<BTd class="text-end">{{
|
||||
renderEuro(data.amount - data.category.month_threshold)
|
||||
}}</BTd>
|
||||
</BTr>
|
||||
</template>
|
||||
</BTbody>
|
||||
|
|
@ -59,13 +60,8 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BTableSimple, BThead, BTbody, BTr, BTh, BTd } from 'bootstrap-vue-next'
|
||||
import { renderCategory, renderEuro } from '../../lib/renderers'
|
||||
import {BTableSimple, BThead, BTbody, BTr, BTh, BTd} from 'bootstrap-vue-next'
|
||||
import {renderCategory, renderEuro} from '../../lib/renderers'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['data'])
|
||||
</script>
|
||||
|
|
@ -12,16 +12,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { compute } from '../../chart/savingAccount'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import { chartStyle } from '../../lib/chartStyle'
|
||||
import {defineProps} from 'vue'
|
||||
import {compute} from '../../chart/savingAccount'
|
||||
import {Bar} from 'vue-chartjs'
|
||||
import {chartStyle} from '../../lib/chartStyle'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
defineProps(['data'])
|
||||
|
||||
const options = () => {
|
||||
return {
|
||||
|
|
@ -29,8 +25,8 @@ const options = () => {
|
|||
maintainAspectRatio: true,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: { stacked: true, ticks: { beginAtZero: true } },
|
||||
x: { stacked: true },
|
||||
y: {stacked: true, ticks: {beginAtZero: true}},
|
||||
x: {stacked: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -5,4 +5,4 @@ const chartStyle = (height) => {
|
|||
}
|
||||
}
|
||||
|
||||
export { chartStyle }
|
||||
export {chartStyle}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ const isInRange = (date, dateFrom, dateTo) => {
|
|||
return true
|
||||
}
|
||||
|
||||
export { isInRange }
|
||||
export {isInRange}
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ const appendRequestQueryFilters = (data, route) => {
|
|||
return data
|
||||
}
|
||||
|
||||
export { queryFilters, appendRequestQueryFilters }
|
||||
export {queryFilters, appendRequestQueryFilters}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ const defineLimits = () => {
|
|||
|
||||
for (let i of [10, 20, 50, 100, 0]) {
|
||||
let label = i !== 0 ? i : 'Tout'
|
||||
v.push({ value: i, text: label })
|
||||
v.push({value: i, text: label})
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
export { defineLimits }
|
||||
export {defineLimits}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const renderDateTime = (value) => {
|
|||
|
||||
const renderCategory = (item) => {
|
||||
if (item !== null) {
|
||||
return `<span class="fa-solid fa-square" style="color: ${item.color}" b> </span> ${item.label}`
|
||||
return `<span class="badge" style="background: ${item.color}"> </span> ${item.label}`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,4 +62,11 @@ const renderLabelWithSum = (label, rows, key) => {
|
|||
return `${label}<br><small class="fw-normal">${renderEuro(sum)}</small>`
|
||||
}
|
||||
|
||||
export { renderDate, renderDateTime, renderCategory, renderBankAccount, renderEuro, renderLabelWithSum }
|
||||
export {
|
||||
renderDate,
|
||||
renderDateTime,
|
||||
renderCategory,
|
||||
renderBankAccount,
|
||||
renderEuro,
|
||||
renderLabelWithSum,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,25 +11,6 @@ const createRequestOptions = (options) => {
|
|||
return options
|
||||
}
|
||||
|
||||
const requestCallback = async (endpoint, options, callback) => {
|
||||
return fetch(endpoint, createRequestOptions(options))
|
||||
.then((response) => response.json())
|
||||
.then(callback)
|
||||
export {
|
||||
createRequestOptions
|
||||
}
|
||||
|
||||
const request = async (endpoint, options) => {
|
||||
return fetch(endpoint, createRequestOptions(options)).then((response) => response.json())
|
||||
}
|
||||
|
||||
const requestErrorBuilder = (response) => {
|
||||
let value = response.message
|
||||
const errors = Object.values(response.errors ?? {})
|
||||
|
||||
if (errors.length) {
|
||||
value += `<ul class="px-3 py-0 my-0 pt-1">${errors.map((e) => `<li class="m-0 p-0">${e}</li>`).join('')}</ul>`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export { createRequestOptions, request, requestCallback, requestErrorBuilder }
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ const removeStorage = function (key) {
|
|||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export { getStorage, saveStorage, removeStorage }
|
||||
export {getStorage, saveStorage, removeStorage}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,36 @@
|
|||
import '../scss/main.scss'
|
||||
import { createApp } from 'vue'
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createBootstrap } from 'bootstrap-vue-next'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, LineElement, PointElement } from 'chart.js'
|
||||
import {createBootstrap} from 'bootstrap-vue-next'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, LineElement, PointElement)
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta?.label) {
|
||||
document.title = `${to.meta.label} - Budget`
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createBootstrap())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
import { requestCallback, request } from '../lib/request'
|
||||
|
||||
const endpoints = {
|
||||
list: '/api/bank_account',
|
||||
item: '/api/bank_account',
|
||||
}
|
||||
|
||||
const getList = async (query, callback) => {
|
||||
return requestCallback(`${endpoints.list}?${new URLSearchParams(query)}`, {}, callback ?? (() => true))
|
||||
}
|
||||
|
||||
const getOne = async (query, callback) => {
|
||||
return request(`${endpoints.list}?${new URLSearchParams(query)}`, {})
|
||||
.then((data) => data.rows[0] ?? null)
|
||||
.then(callback ?? (() => true))
|
||||
}
|
||||
|
||||
const write = async (endpoint, data, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const create = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const update = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}/${data.id}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const remove = async (id, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
`${endpoints.item}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const createForm = (item) => {
|
||||
const data = { ...item }
|
||||
|
||||
return {
|
||||
data: data,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const getListFields = () => {
|
||||
return [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export { endpoints, getList, getOne, create, update, remove, getListFields, createForm }
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
import { requestCallback, request } from '../lib/request'
|
||||
import { renderCategory, renderEuro, renderLabelWithSum } from '../lib/renderers'
|
||||
|
||||
const endpoints = {
|
||||
list: '/api/category',
|
||||
item: '/api/category',
|
||||
}
|
||||
|
||||
const getList = async (query, callback) => {
|
||||
return requestCallback(`${endpoints.list}?${new URLSearchParams(query)}`, {}, callback ?? (() => true))
|
||||
}
|
||||
|
||||
const getOne = async (query, callback) => {
|
||||
return request(`${endpoints.list}?${new URLSearchParams(query)}`, {})
|
||||
.then((data) => data.rows[0] ?? null)
|
||||
.then(callback ?? (() => true))
|
||||
}
|
||||
|
||||
const write = async (endpoint, data, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(fixData(data)),
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const create = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const update = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}/${data.id}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const fixData = (data) => {
|
||||
if (data.month_threshold !== null && data.month_threshold !== '') {
|
||||
data.month_threshold = parseFloat(data.month_threshold)
|
||||
} else {
|
||||
data.month_threshold = null
|
||||
}
|
||||
|
||||
if (data.rules === null) {
|
||||
data.rules = []
|
||||
}
|
||||
|
||||
data.rules.forEach((value, key) => {
|
||||
if (value.amount !== null && value.amount !== '') {
|
||||
data.rules[key].amount = parseFloat(value.amount)
|
||||
}
|
||||
|
||||
if (value.date_from) {
|
||||
data.rules[key].date_from = toISODateString(value.date_from, 0, 0, 0)
|
||||
}
|
||||
|
||||
if (value.date_to) {
|
||||
data.rules[key].date_to = toISODateString(value.date_to, 23, 59, 59)
|
||||
}
|
||||
|
||||
for (let i in value) {
|
||||
if (value[i] === '') {
|
||||
data.rules[key][i] = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
data.ignore_transactions = data.ignore_transactions === 1
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const remove = async (id, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
`${endpoints.item}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const createForm = (item) => {
|
||||
if (!item) {
|
||||
item = {
|
||||
id: null,
|
||||
label: null,
|
||||
color: '#9eb1e7',
|
||||
rules: [],
|
||||
month_threshold: null,
|
||||
ignore_transactions: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const data = { ...item }
|
||||
|
||||
if (data.rules) {
|
||||
data.rules.forEach((value, key) => {
|
||||
if (value.date_from) {
|
||||
data.rules[key].date_from = fromISODateString(value.date_from)
|
||||
}
|
||||
|
||||
if (value.date_to) {
|
||||
data.rules[key].date_to = fromISODateString(value.date_to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data.ignore_transactions = data.ignore_transactions ? 1 : 0
|
||||
|
||||
return {
|
||||
data: data,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Seuil mensuel',
|
||||
type: 'number',
|
||||
required: false,
|
||||
key: 'month_threshold',
|
||||
},
|
||||
{
|
||||
label: 'Ignorer les transactions liées',
|
||||
widget: 'select',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 0, text: 'Non' },
|
||||
{ value: 1, text: 'Oui' },
|
||||
],
|
||||
key: 'ignore_transactions',
|
||||
},
|
||||
{
|
||||
label: 'Couleur',
|
||||
type: 'color',
|
||||
required: true,
|
||||
key: 'color',
|
||||
},
|
||||
{
|
||||
label: null,
|
||||
widget: 'rules',
|
||||
required: true,
|
||||
key: 'rules',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const getListFields = () => {
|
||||
return [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
render: (item) => renderCategory(item),
|
||||
},
|
||||
{
|
||||
key: 'month_threshold',
|
||||
renderLabel: (rows) => renderLabelWithSum('Seuil mensuel', rows, 'month_threshold'),
|
||||
width: '150px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.month_threshold),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const toISODateString = (v, h, m, s) => {
|
||||
const d = new Date(v)
|
||||
d.setUTCHours(h)
|
||||
d.setUTCMinutes(m)
|
||||
d.setUTCSeconds(s)
|
||||
|
||||
return d.toISOString()
|
||||
}
|
||||
|
||||
const fromISODateString = (v) => {
|
||||
return v.split('T', 1)[0]
|
||||
}
|
||||
|
||||
export { endpoints, getList, getOne, create, update, remove, getListFields, createForm }
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import { requestCallback, request } from '../lib/request'
|
||||
import { renderEuro, renderLabelWithSum } from '../lib/renderers'
|
||||
|
||||
const endpoints = {
|
||||
list: '/api/saving_account',
|
||||
item: '/api/saving_account',
|
||||
}
|
||||
|
||||
const getList = async (query, callback) => {
|
||||
return requestCallback(`${endpoints.list}?${new URLSearchParams(query)}`, {}, callback ?? (() => true))
|
||||
}
|
||||
|
||||
const getOne = async (query, callback) => {
|
||||
return request(`${endpoints.list}?${new URLSearchParams(query)}`, {})
|
||||
.then((data) => data.rows[0] ?? null)
|
||||
.then(callback ?? (() => true))
|
||||
}
|
||||
|
||||
const write = async (endpoint, data, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(fixData(data)),
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const create = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}`, fixData(data), callback, callbackError)
|
||||
}
|
||||
|
||||
const update = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}/${data.id}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const remove = async (id, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
`${endpoints.item}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const fixData = (data) => {
|
||||
if (data.blocked_amount) {
|
||||
data.blocked_amount = parseFloat(data.blocked_amount)
|
||||
} else {
|
||||
data.blocked_amount = 0.0
|
||||
}
|
||||
|
||||
if (data.released_amount) {
|
||||
data.released_amount = parseFloat(data.released_amount)
|
||||
} else {
|
||||
data.released_amount = 0.0
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const createForm = (item) => {
|
||||
if (!item) {
|
||||
item = {
|
||||
id: null,
|
||||
label: null,
|
||||
color: '#9eb1e7',
|
||||
rules: [],
|
||||
month_threshold: null,
|
||||
ignore_transactions: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const data = { ...item }
|
||||
|
||||
return {
|
||||
data: data,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Montant bloqué',
|
||||
type: 'number',
|
||||
key: 'blocked_amount',
|
||||
},
|
||||
{
|
||||
label: 'Montant débloqué',
|
||||
type: 'number',
|
||||
key: 'released_amount',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const getListFields = () => {
|
||||
return [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
{
|
||||
key: 'blocked_amount',
|
||||
renderLabel: (rows) => renderLabelWithSum('Montant bloqué', rows, 'blocked_amount'),
|
||||
width: '200px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.blocked_amount),
|
||||
},
|
||||
{
|
||||
key: 'released_amount',
|
||||
renderLabel: (rows) => renderLabelWithSum('Montant débloqué', rows, 'released_amount'),
|
||||
width: '200px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.released_amount),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export { endpoints, getList, getOne, create, update, remove, createForm, getListFields }
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
import { requestCallback, request } from '../lib/request'
|
||||
import { renderDate, renderCategory, renderBankAccount, renderEuro, renderLabelWithSum } from '../lib/renderers'
|
||||
|
||||
const endpoints = {
|
||||
list: '/api/transaction',
|
||||
items: '/api/transactions',
|
||||
updateCategories: '/api/transactions/update_categories',
|
||||
}
|
||||
|
||||
const getList = async (query, callback) => {
|
||||
return requestCallback(`${endpoints.list}?${new URLSearchParams(query)}`, {}, callback ?? (() => true))
|
||||
}
|
||||
|
||||
const getOne = async (query, callback) => {
|
||||
return request(`${endpoints.list}?${new URLSearchParams(query)}`, {})
|
||||
.then((data) => data.rows[0] ?? null)
|
||||
.then(callback ?? (() => true))
|
||||
}
|
||||
|
||||
const importFile = async (data, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
endpoints.items,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: createPayload(data),
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const createPayload = (data) => {
|
||||
const payload = new FormData()
|
||||
|
||||
payload.append('bank_account_id', data.bank_account_id)
|
||||
payload.append('file', data.file)
|
||||
payload.append('format', data.format)
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
const createForm = (accounts) => {
|
||||
const data = { category_id: null, file: null, format: 'caisse_epargne' }
|
||||
|
||||
const options = []
|
||||
|
||||
accounts.forEach((item) => {
|
||||
options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
data: data,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Compte bancaire',
|
||||
widget: 'select',
|
||||
options: options,
|
||||
required: true,
|
||||
key: 'bank_account_id',
|
||||
},
|
||||
{
|
||||
label: 'Format de fichier',
|
||||
key: 'format',
|
||||
widget: 'select',
|
||||
options: [
|
||||
{ value: 'caisse_epargne', text: "Caisse d'épargne" },
|
||||
{ value: 'banque_populaire', text: "Banque Populaire" },
|
||||
{ value: 'revolut', text: 'Revolut' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Fichier',
|
||||
description: `Fichier CSV des opérations`,
|
||||
widget: 'file',
|
||||
required: true,
|
||||
key: 'file',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const updateCategories = async (callback, callbackError) => {
|
||||
return requestCallback(
|
||||
`${endpoints.updateCategories}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const getListFields = () => {
|
||||
return [
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
width: '90px',
|
||||
render: (item) => renderDate(item.date),
|
||||
},
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
{
|
||||
key: 'operation_type',
|
||||
width: '200px',
|
||||
label: 'Type',
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
label: 'Catégorie',
|
||||
orderKey: 'category_id',
|
||||
width: '400px',
|
||||
render: (item) => renderCategory(item.category),
|
||||
},
|
||||
{
|
||||
key: 'bank_account',
|
||||
label: 'Compte',
|
||||
width: '200px',
|
||||
orderKey: 'bank_account_id',
|
||||
render: (item) => renderBankAccount(item.bank_account),
|
||||
},
|
||||
{
|
||||
key: 'debit',
|
||||
renderLabel: (rows) => renderLabelWithSum('Débit', rows, 'debit'),
|
||||
width: '120px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.debit),
|
||||
},
|
||||
{
|
||||
key: 'credit',
|
||||
renderLabel: (rows) => renderLabelWithSum('Crédit', rows, 'credit'),
|
||||
width: '120px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.credit),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const getListFilters = () => {
|
||||
return [
|
||||
{ key: 'label', type: 'string', label: 'Libellé' },
|
||||
{ key: 'debit', type: 'number', label: 'Débit' },
|
||||
{ key: 'credit', type: 'number', label: 'Crédit' },
|
||||
{ key: 'date', type: 'date', label: 'Date' },
|
||||
{ key: 'operation_type', type: 'string', label: 'Type' },
|
||||
{ key: 'category_id', type: 'select', label: 'Catégorie', options: [] },
|
||||
{ key: 'bank_account_id', type: 'select', label: 'Compte', options: [] },
|
||||
]
|
||||
}
|
||||
|
||||
export { endpoints, getList, getOne, createForm, getListFields, getListFilters, importFile, updateCategories }
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { requestCallback, request } from '../lib/request'
|
||||
import { renderDateTime } from '../lib/renderers'
|
||||
|
||||
const endpoints = {
|
||||
list: '/api/user',
|
||||
item: '/api/user',
|
||||
}
|
||||
|
||||
const getList = async (query, callback) => {
|
||||
return requestCallback(`${endpoints.list}?${new URLSearchParams(query)}`, {}, callback ?? (() => true))
|
||||
}
|
||||
|
||||
const getOne = async (query, callback) => {
|
||||
return request(`${endpoints.list}?${new URLSearchParams(query)}`, {})
|
||||
.then((data) => data.rows[0] ?? null)
|
||||
.then(callback ?? (() => true))
|
||||
}
|
||||
|
||||
const write = async (endpoint, data, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const create = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const update = async (data, callback, callbackError) => {
|
||||
return write(`${endpoints.item}/${data.id}`, data, callback, callbackError)
|
||||
}
|
||||
|
||||
const remove = async (id, callback, callbackError) => {
|
||||
return requestCallback(
|
||||
`${endpoints.item}/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
callback ?? (() => true),
|
||||
).catch(callbackError ?? (() => true))
|
||||
}
|
||||
|
||||
const createForm = (item) => {
|
||||
const data = { ...item }
|
||||
|
||||
return {
|
||||
data: data,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nom',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'display_name',
|
||||
},
|
||||
{
|
||||
label: "Nom d'utilisateur",
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
label: 'Mot de passe',
|
||||
type: 'password',
|
||||
required: data.id > 0,
|
||||
key: 'password',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const getListFields = () => {
|
||||
return [
|
||||
{
|
||||
key: 'display_name',
|
||||
label: 'Nom',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Utilisateur',
|
||||
},
|
||||
{
|
||||
key: 'logged_at',
|
||||
label: 'Dernière connexion',
|
||||
render: (item) => renderDateTime(item.logged_at),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export { endpoints, getList, getOne, create, update, remove, getListFields, createForm }
|
||||
|
|
@ -1,137 +1,48 @@
|
|||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'dashboard',
|
||||
meta: { label: 'Tableau de bord', icon: ['fa-solid', 'fa-chart-line'] },
|
||||
name: 'Tableau de bord',
|
||||
meta: {icon: ['fa-solid', 'fa-chart-line']},
|
||||
component: () => import('../views/DashboardView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/transactions',
|
||||
name: 'transactions',
|
||||
meta: {
|
||||
label: 'Transactions',
|
||||
icon: ['fa-solid', 'fa-money-bill-transfer'],
|
||||
},
|
||||
component: () => import('../views/transaction/ListView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/transaction/:id',
|
||||
props: true,
|
||||
name: 'transaction_show',
|
||||
component: () => import('../views/transaction/ShowView.vue'),
|
||||
},
|
||||
],
|
||||
name: 'Transactions',
|
||||
meta: {icon: ['fa-solid', 'fa-money-bill-transfer']},
|
||||
component: () => import('../views/TransactionsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
component: () => import('../views/ComponentView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'users',
|
||||
meta: { label: 'Utilisateurs', icon: ['fa-solid', 'fa-users'] },
|
||||
component: () => import('../views/user/ListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/user/edit/:id',
|
||||
name: 'user_edit',
|
||||
meta: { label: 'Utilisateur' },
|
||||
component: () => import('../views/user/EditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/user/create',
|
||||
name: 'user_create',
|
||||
meta: { label: 'Nouvel utilisateur' },
|
||||
component: () => import('../views/user/CreateView.vue'),
|
||||
},
|
||||
],
|
||||
name: 'Utilisateurs',
|
||||
meta: {icon: ['fa-solid', 'fa-users']},
|
||||
component: () => import('../views/UsersView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
component: () => import('../views/ComponentView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'categories',
|
||||
meta: { label: 'Catégories', icon: ['fa-solid', 'fa-list'] },
|
||||
component: () => import('../views/category/ListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/category/edit/:id/:transaction?',
|
||||
name: 'category_edit',
|
||||
meta: { label: 'Catégorie' },
|
||||
component: () => import('../views/category/EditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/category/create',
|
||||
name: 'category_create',
|
||||
meta: { label: 'Nouvelle catégorie' },
|
||||
component: () => import('../views/category/CreateView.vue'),
|
||||
},
|
||||
],
|
||||
name: 'Catégories',
|
||||
meta: {icon: ['fa-solid', 'fa-list']},
|
||||
component: () => import('../views/CategoriesView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bank_accounts',
|
||||
component: () => import('../views/ComponentView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'bank_accounts',
|
||||
meta: {
|
||||
label: 'Comptes bancaires',
|
||||
icon: ['fa-solid', 'fa-piggy-bank'],
|
||||
},
|
||||
component: () => import('../views/bank_account/ListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bank_account/edit/:id',
|
||||
name: 'bank_account_edit',
|
||||
meta: { label: 'Compte bancaire' },
|
||||
component: () => import('../views/bank_account/EditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bank_account/create',
|
||||
name: 'bank_account_create',
|
||||
meta: { label: 'Nouveau compte bancaire' },
|
||||
component: () => import('../views/bank_account/CreateView.vue'),
|
||||
},
|
||||
],
|
||||
name: 'Comptes bancaires',
|
||||
meta: {icon: ['fa-solid', 'fa-piggy-bank']},
|
||||
component: () => import('../views/BankAccountsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/saving_accounts',
|
||||
component: () => import('../views/ComponentView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'saving_accounts',
|
||||
meta: {
|
||||
label: 'Comptes épargnes',
|
||||
icon: ['fa-solid', 'fa-piggy-bank'],
|
||||
},
|
||||
component: () => import('../views/saving_account/ListView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/saving_account/edit/:id',
|
||||
name: 'saving_account_edit',
|
||||
meta: { label: 'Compte bancaire' },
|
||||
component: () => import('../views/saving_account/EditView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/saving_account/create',
|
||||
name: 'saving_account_create',
|
||||
meta: { label: 'Nouveau compte épargne' },
|
||||
component: () => import('../views/saving_account/CreateView.vue'),
|
||||
},
|
||||
],
|
||||
name: 'Comptes épargnes',
|
||||
meta: {icon: ['fa-solid', 'fa-piggy-bank']},
|
||||
component: () => import('../views/SavingAccountsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
name: 'files',
|
||||
meta: { label: 'Fichiers', icon: ['fa-solid', 'fa-table'] },
|
||||
name: 'Fichiers',
|
||||
meta: {icon: ['fa-solid', 'fa-table']},
|
||||
component: () => import('../views/FilesView.vue'),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
319
frontend/js/views/BankAccountsView.vue
Normal file
319
frontend/js/views/BankAccountsView.vue
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<Header title="Comptes bancaires">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<Pager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
|
||||
<div class="crud-list">
|
||||
<BTableSimple
|
||||
v-if="data !== null"
|
||||
caption-top
|
||||
responsive
|
||||
>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="(field, key) in fields"
|
||||
:key="key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.classes"
|
||||
valign="top"
|
||||
@click="doSort(field.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="field.orderKey ?? field.key"
|
||||
:label="field.label ?? field.renderLabel(data.rows)"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="row in data.rows"
|
||||
:key="row.id"
|
||||
>
|
||||
<BTd
|
||||
v-for="(field, key) in fields"
|
||||
:key="key"
|
||||
class="cursor"
|
||||
@click="doEdit(row)"
|
||||
>
|
||||
<span v-if="field.key">
|
||||
<span
|
||||
v-if="field.render"
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[field.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</div>
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
v-model="formShow"
|
||||
:title="form?.label"
|
||||
footer-class="justify-content-between"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
</BFormGroup>
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="form.data.id"
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BTbody,
|
||||
BThead,
|
||||
BTr,
|
||||
BTd,
|
||||
BTh,
|
||||
BContainer,
|
||||
BTableSimple,
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BButtonToolbar,
|
||||
} from 'bootstrap-vue-next'
|
||||
|
||||
import SortButton from './../components/SortButton.vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import Pager from './../components/crud/Pager.vue'
|
||||
import {ref, onMounted, watch} from 'vue'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
const endpoint = `/api/bank_account`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
fetch(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})}`,
|
||||
createRequestOptions()
|
||||
)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (value) {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
const data = {...item}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}/${item.id}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: data.label,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
const data = {label: null}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: 'Nouveau',
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})).then(() => {
|
||||
formShow.value = false
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const url = form.value.data.id
|
||||
? `${endpoint}/${form.value.data.id}`
|
||||
: endpoint
|
||||
|
||||
fetch(url, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = data.message
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
648
frontend/js/views/CategoriesView.vue
Normal file
648
frontend/js/views/CategoriesView.vue
Normal file
|
|
@ -0,0 +1,648 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<Header title="Catégories">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="doApply"
|
||||
>
|
||||
<VueSpinner
|
||||
v-if="applyInProgress"
|
||||
size="20"
|
||||
color="white"
|
||||
/>
|
||||
<span v-else>Appliquer les règles</span>
|
||||
</BButton>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<Pager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
|
||||
<div class="crud-list">
|
||||
<BTableSimple
|
||||
v-if="data !== null"
|
||||
caption-top
|
||||
responsive
|
||||
>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.thClasses"
|
||||
valign="top"
|
||||
@click="doSort(field.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="field.orderKey ?? field.key"
|
||||
:label="field.label ?? field.renderLabel(data.rows)"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="(row, key) in data.rows"
|
||||
:key="key"
|
||||
>
|
||||
<BTd
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="cursor"
|
||||
:class="field.tdClasses"
|
||||
@click="doEdit(row)"
|
||||
>
|
||||
<span v-if="field.key">
|
||||
<span
|
||||
v-if="field.render"
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[field.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</div>
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
ref="modal"
|
||||
v-model="formShow"
|
||||
scrollable
|
||||
class="modal-xl"
|
||||
:title="form?.label"
|
||||
footer-class="justify-content-between"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<BFormSelect
|
||||
v-if="(field.widget ?? 'text') === 'select'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:options="field.options"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<div v-if="field.widget === 'rules'">
|
||||
<div
|
||||
v-if="
|
||||
form.data[field.key] !== null && form.data[field.key].length > 0
|
||||
"
|
||||
class="list-group"
|
||||
>
|
||||
<div
|
||||
v-for="(rule, key) in form.data[field.key]"
|
||||
:key="key"
|
||||
class="list-group-item"
|
||||
>
|
||||
<div class="d-block d-lg-flex justify-content-between gap-1">
|
||||
<BFormInput
|
||||
v-if="form.data[field.key][key].id !== null"
|
||||
v-model="form.data[field.key][key].id"
|
||||
type="hidden"
|
||||
/>
|
||||
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Libellé contient"
|
||||
:label-for="'form-rule-contain-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-contain-' + key"
|
||||
v-model="form.data[field.key][key].contain"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Regex libellé"
|
||||
:label-for="'form-rule-match-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-match-' + key"
|
||||
v-model="form.data[field.key][key].match"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Catégorie banque"
|
||||
:label-for="'form-rule-contain-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-bank-category-contain-' + key"
|
||||
v-model="form.data[field.key][key].bank_category"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Montant"
|
||||
:label-for="'form-rule-amount-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-amount-' + key"
|
||||
v-model="form.data[field.key][key].amount"
|
||||
type="number"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Du"
|
||||
:label-for="'form-rule-datefrom-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-datefrom-' + key"
|
||||
v-model="form.data[field.key][key].date_from"
|
||||
type="date"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
class="mb-2"
|
||||
label="Au"
|
||||
:label-for="'form-rule-dateto-' + key"
|
||||
>
|
||||
<BFormInput
|
||||
:id="'form-rule-dateto-' + key"
|
||||
v-model="form.data[field.key][key].date_to"
|
||||
type="date"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BButton
|
||||
variant="none"
|
||||
@click="
|
||||
form.data[field.key] = doRemoveRule(
|
||||
form.data[field.key],
|
||||
key,
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BFormGroup>
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="form.data['rules'] = doAddRule(form.data['rules'])"
|
||||
>Ajouter règle</BButton
|
||||
>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="form.data.id"
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BTbody,
|
||||
BThead,
|
||||
BTr,
|
||||
BTd,
|
||||
BTh,
|
||||
BContainer,
|
||||
BTableSimple,
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BButtonToolbar,
|
||||
BFormSelect,
|
||||
} from 'bootstrap-vue-next'
|
||||
|
||||
import {VueSpinner} from 'vue3-spinners'
|
||||
import SortButton from './../components/SortButton.vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import Pager from './../components/crud/Pager.vue'
|
||||
import {ref, onMounted, watch, useTemplateRef} from 'vue'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {renderCategory, renderEuro, renderLabelWithSum} from '../lib/renderers'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
const endpoint = `/api/category`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
const applyInProgress = ref(false)
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const doRemoveRule = (item, key) => {
|
||||
let values = []
|
||||
|
||||
item.forEach((v, k) => {
|
||||
if (k !== key) {
|
||||
values.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
const doAddRule = (item) => {
|
||||
const rule = {
|
||||
contain: null,
|
||||
match: null,
|
||||
bank_category: null,
|
||||
amount: null,
|
||||
date_from: null,
|
||||
date_to: null,
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = []
|
||||
}
|
||||
|
||||
item.push(rule)
|
||||
|
||||
window.setTimeout(() => {
|
||||
const modalBody = document.querySelector('.modal-body')
|
||||
modalBody.scrollTo({top: modalBody.scrollHeight, behavior: 'smooth'})
|
||||
}, 300)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
const doApply = () => {
|
||||
applyInProgress.value = true
|
||||
fetch('/api/transactions/update_categories', createRequestOptions({method: 'POST'})).then(() => {
|
||||
applyInProgress.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetch(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})}`,
|
||||
createRequestOptions()
|
||||
)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (value) {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const toISODateString = (v, h, m, s) => {
|
||||
const d = new Date(v)
|
||||
d.setUTCHours(h)
|
||||
d.setUTCMinutes(m)
|
||||
d.setUTCSeconds(s)
|
||||
|
||||
return d.toISOString()
|
||||
}
|
||||
|
||||
const fromISODateString = (v) => {
|
||||
return v.split('T', 1)[0]
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
const data = {...item}
|
||||
|
||||
if (data.rules !== null) {
|
||||
data.rules.forEach((value, key) => {
|
||||
if (value.date_from) {
|
||||
data.rules[key].date_from = fromISODateString(value.date_from)
|
||||
}
|
||||
|
||||
if (value.date_to) {
|
||||
data.rules[key].date_to = fromISODateString(value.date_to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data.ignore_transactions = data.ignore_transactions ? 1 : 0
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}/${item.id}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: data.label,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Seuil mensuel',
|
||||
type: 'number',
|
||||
required: false,
|
||||
key: 'month_threshold',
|
||||
},
|
||||
{
|
||||
label: 'Ignorer les transactions liées',
|
||||
widget: 'select',
|
||||
required: false,
|
||||
options: [
|
||||
{value: 0, text: 'Non'},
|
||||
{value: 1, text: 'Oui'},
|
||||
],
|
||||
key: 'ignore_transactions',
|
||||
},
|
||||
{
|
||||
label: 'Couleur',
|
||||
type: 'color',
|
||||
required: true,
|
||||
key: 'color',
|
||||
},
|
||||
{
|
||||
label: null,
|
||||
widget: 'rules',
|
||||
required: false,
|
||||
key: 'rules',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
const data = {
|
||||
id: null,
|
||||
label: null,
|
||||
color: '#9eb1e7',
|
||||
rules: [],
|
||||
month_threshold: null,
|
||||
ignore_transactions: 0,
|
||||
}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: 'Nouveau',
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: null,
|
||||
type: 'hidden',
|
||||
required: false,
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Seuil mensuel',
|
||||
type: 'number',
|
||||
required: false,
|
||||
key: 'month_threshold',
|
||||
},
|
||||
{
|
||||
label: 'Ignorer les transactions liées',
|
||||
widget: 'select',
|
||||
required: false,
|
||||
options: [
|
||||
{value: 0, text: 'Non'},
|
||||
{value: 1, text: 'Oui'},
|
||||
],
|
||||
key: 'ignore_transactions',
|
||||
},
|
||||
{
|
||||
label: 'Couleur',
|
||||
type: 'color',
|
||||
required: true,
|
||||
key: 'color',
|
||||
},
|
||||
{
|
||||
label: null,
|
||||
widget: 'rules',
|
||||
required: true,
|
||||
key: 'rules',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})).then(() => {
|
||||
formShow.value = false
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const url = form.value.data.id
|
||||
? `${endpoint}/${form.value.data.id}`
|
||||
: endpoint
|
||||
|
||||
if (form.value.data.rules === null) {
|
||||
form.value.data.rules = []
|
||||
}
|
||||
|
||||
if (
|
||||
form.value.data.month_threshold !== null &&
|
||||
form.value.data.month_threshold !== ''
|
||||
) {
|
||||
form.value.data.month_threshold = parseFloat(
|
||||
form.value.data.month_threshold,
|
||||
)
|
||||
} else {
|
||||
form.value.data.month_threshold = null
|
||||
}
|
||||
|
||||
form.value.data.ignore_transactions =
|
||||
form.value.data.ignore_transactions === 1
|
||||
|
||||
form.value.data.rules.forEach((value, key) => {
|
||||
if (value.amount !== null && value.amount !== '') {
|
||||
form.value.data.rules[key].amount = parseFloat(value.amount)
|
||||
}
|
||||
|
||||
if (value.date_from) {
|
||||
form.value.data.rules[key].date_from = toISODateString(
|
||||
value.date_from,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
if (value.date_to) {
|
||||
form.value.data.rules[key].date_to = toISODateString(
|
||||
value.date_to,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
)
|
||||
}
|
||||
|
||||
for (let i in value) {
|
||||
if (value[i] === '') {
|
||||
form.value.data.rules[key][i] = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fetch(url, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = data.message
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
render: (item) => renderCategory(item),
|
||||
},
|
||||
{
|
||||
key: 'month_threshold',
|
||||
renderLabel: (rows) =>
|
||||
renderLabelWithSum('Seuil mensuel', rows, 'month_threshold'),
|
||||
width: '150px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.month_threshold),
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
<template>
|
||||
<div class="p-3">
|
||||
<div v-if="!isLoading && data !== null && accounts.length > 0 && categories.length > 0">
|
||||
<div
|
||||
v-if="
|
||||
!isLoading &&
|
||||
data !== null &&
|
||||
accounts.length > 0 &&
|
||||
categories.length > 0
|
||||
"
|
||||
>
|
||||
<div class="float-end">
|
||||
<BButton
|
||||
variant="none"
|
||||
|
|
@ -10,7 +17,7 @@
|
|||
</BButton>
|
||||
</div>
|
||||
|
||||
<DashFilter
|
||||
<Filters
|
||||
v-model:account="account"
|
||||
v-model:date-from="dateFrom"
|
||||
v-model:date-to="dateTo"
|
||||
|
|
@ -42,51 +49,50 @@
|
|||
>
|
||||
<BButton
|
||||
v-for="i in [
|
||||
{ size: 4, label: '4' },
|
||||
{ size: 6, label: '6' },
|
||||
{ size: 8, label: '8' },
|
||||
{ size: 10, label: '10' },
|
||||
{ size: 12, label: '12' },
|
||||
{size: 4, label: '4'},
|
||||
{size: 6, label: '6'},
|
||||
{size: 8, label: '8'},
|
||||
{size: 10, label: '10'},
|
||||
{size: 12, label: '12'},
|
||||
]"
|
||||
:key="i"
|
||||
:variant="i.size == config[key].size ? 'primary' : 'secondary'"
|
||||
@click="setConfigSize(key, i.size)"
|
||||
@click="config[key].size = i.size; updateConfig()"
|
||||
>{{ i.label }}</BButton
|
||||
>
|
||||
</BButtonGroup>
|
||||
</div>
|
||||
|
||||
<CapitalChart
|
||||
v-if="item.component === 'CapitalChart'"
|
||||
<Capital
|
||||
v-if="item.component === 'Capital'"
|
||||
:data="data"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
/>
|
||||
<SavingAccountsChart
|
||||
v-if="item.component === 'SavingAccountsChart'"
|
||||
<SavingAccounts
|
||||
v-if="item.component === 'SavingAccounts'"
|
||||
:data="savingAccounts"
|
||||
/>
|
||||
<DistributionChart
|
||||
v-if="item.component === 'DistributionChart'"
|
||||
<Distribution
|
||||
v-if="item.component === 'Distribution'"
|
||||
:data="data"
|
||||
:categories="categories"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
/>
|
||||
<DiffCreditDebitChart
|
||||
v-if="item.component === 'DiffCreditDebitChart'"
|
||||
<DiffCreditDebit
|
||||
v-if="item.component === 'DiffCreditDebit'"
|
||||
:data="data"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
/>
|
||||
<MonthThresholdsTable
|
||||
v-if="item.component === 'MonthThresholdsTable'"
|
||||
<MonthThresholds
|
||||
v-if="item.component === 'MonthThresholds'"
|
||||
:data="monthThresholdsData()"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
/>
|
||||
<CategoriesChart
|
||||
v-if="item.component === 'CategoriesChart'"
|
||||
<CategoriesStats
|
||||
v-if="item.component === 'CategoriesStats'"
|
||||
:data="data"
|
||||
:date-from="dateFrom"
|
||||
:date-to="dateTo"
|
||||
|
|
@ -95,30 +101,24 @@
|
|||
</transition-group>
|
||||
</Draggable>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5"
|
||||
>
|
||||
<p>Chargement...</p>
|
||||
<BSpinner />
|
||||
</div>
|
||||
<div v-else>Chargement...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { compute as monthThresholds } from '../chart/monthThreshold'
|
||||
import { getStorage, saveStorage } from '../lib/storage'
|
||||
import { BButtonGroup, BButton, BSpinner } from 'bootstrap-vue-next'
|
||||
import DashFilter from './../components/dashboard/DashFilter.vue'
|
||||
import CapitalChart from './../components/dashboard/CapitalChart.vue'
|
||||
import SavingAccountsChart from './../components/dashboard/SavingAccountsChart.vue'
|
||||
import DistributionChart from './../components/dashboard/DistributionChart.vue'
|
||||
import MonthThresholdsTable from './../components/dashboard/MonthThresholdsTable.vue'
|
||||
import DiffCreditDebitChart from './../components/dashboard/DiffCreditDebitChart.vue'
|
||||
import CategoriesChart from './../components/dashboard/CategoriesChart.vue'
|
||||
import { VueDraggableNext as Draggable } from 'vue-draggable-next'
|
||||
import { createRequestOptions } from '../lib/request'
|
||||
import {ref, reactive, onMounted, watch} from 'vue'
|
||||
import {compute as monthThresholds} from '../chart/monthThreshold'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {BButtonGroup, BButton} from 'bootstrap-vue-next'
|
||||
import Filters from './../components/dashboard/Filters.vue'
|
||||
import Capital from './../components/dashboard/Capital.vue'
|
||||
import SavingAccounts from './../components/dashboard/SavingAccounts.vue'
|
||||
import Distribution from './../components/dashboard/Distribution.vue'
|
||||
import MonthThresholds from './../components/dashboard/MonthThresholds.vue'
|
||||
import DiffCreditDebit from './../components/dashboard/DiffCreditDebit.vue'
|
||||
import CategoriesStats from './../components/dashboard/CategoriesStats.vue'
|
||||
import {VueDraggableNext as Draggable} from 'vue-draggable-next'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
const data = ref(null)
|
||||
const isLoading = ref(true)
|
||||
|
|
@ -129,12 +129,12 @@ const savingAccounts = ref([])
|
|||
|
||||
const mode = ref('view')
|
||||
const defaultComponents = [
|
||||
{ component: 'CapitalChart', size: 8 },
|
||||
{ component: 'SavingAccountsChart', size: 4 },
|
||||
{ component: 'DistributionChart', size: 12 },
|
||||
{ component: 'DiffCreditDebitChart', size: 12 },
|
||||
{ component: 'CategoriesChart', size: 12 },
|
||||
{ component: 'MonthThresholdsTable', size: 12 },
|
||||
{component: 'Capital', size: 8},
|
||||
{component: 'SavingAccounts', size: 4},
|
||||
{component: 'Distribution', size: 12},
|
||||
{component: 'DiffCreditDebit', size: 12},
|
||||
{component: 'CategoriesStats', size: 12},
|
||||
{component: 'MonthThresholds', size: 12},
|
||||
]
|
||||
|
||||
const account = ref(getStorage(`dashboard:account`))
|
||||
|
|
@ -149,7 +149,12 @@ const monthThresholdsData = () => {
|
|||
return _monthThresholdsData.value
|
||||
}
|
||||
|
||||
_monthThresholdsData.value = monthThresholds(data.value, categories.value, dateFrom.value, dateTo.value)
|
||||
_monthThresholdsData.value = monthThresholds(
|
||||
data.value,
|
||||
categories.value,
|
||||
dateFrom.value,
|
||||
dateTo.value,
|
||||
)
|
||||
|
||||
return _monthThresholdsData.value
|
||||
}
|
||||
|
|
@ -184,11 +189,6 @@ window.addEventListener('resize', () => {
|
|||
}, 500)
|
||||
})
|
||||
|
||||
const setConfigSize = (key, value) => {
|
||||
config.value[key].size = value
|
||||
updateConfig()
|
||||
}
|
||||
|
||||
const updateConfig = () => {
|
||||
saveStorage(`dashboard:config`, config.value)
|
||||
refresh()
|
||||
|
|
@ -211,7 +211,9 @@ const refresh = () => {
|
|||
fetch(`/api/transaction?${new URLSearchParams(query)}`, createRequestOptions())
|
||||
.then((response) => response.json())
|
||||
.then((value) => {
|
||||
data.value = value.rows.filter((row) => !row.category || row.category.ignore_transactions === false)
|
||||
data.value = value.rows.filter(
|
||||
(row) => !row.category || row.category.ignore_transactions === false,
|
||||
)
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
|
@ -225,11 +227,7 @@ onMounted(() => {
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
categories.value = data.rows
|
||||
categories.value.push({
|
||||
id: -1,
|
||||
label: 'Sans catégorie',
|
||||
color: '#cccccc',
|
||||
})
|
||||
categories.value.push({id: -1, label: 'Sans catégorie', color: '#cccccc'})
|
||||
})
|
||||
|
||||
fetch('/api/bank_account', createRequestOptions())
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-if="tree !== null && fileId === null"
|
||||
class="w-100"
|
||||
>
|
||||
<CrudHeader title="Fichiers">
|
||||
<Header title="Fichiers">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
</Header>
|
||||
|
||||
<div
|
||||
v-if="!tree.is_root"
|
||||
|
|
@ -85,15 +85,10 @@
|
|||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="p-3 border-bottom cursor"
|
||||
class="text-secondary p-3 border-bottom"
|
||||
>
|
||||
<a
|
||||
:href="`/api/filemanager/file/${item.id}/${item.name}`"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-regular fa-file"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
<i class="fa-regular fa-file"></i>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -110,7 +105,6 @@
|
|||
</div>
|
||||
<iframe
|
||||
id="collabora"
|
||||
title="Fichier"
|
||||
:src="'/collabora/' + fileId + '?extension=' + fileExtension"
|
||||
></iframe>
|
||||
</div>
|
||||
|
|
@ -124,8 +118,8 @@
|
|||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
>{{ form.error }}</BAlert
|
||||
>
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
|
|
@ -180,10 +174,21 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import CrudHeader from './../components/crud/CrudHeader.vue'
|
||||
import { BModal, BButton, BForm, BFormGroup, BFormInput, BAlert, BFormSelect, BFormFile, BDropdown, BDropdownItem } from 'bootstrap-vue-next'
|
||||
import { createRequestOptions } from '../lib/request'
|
||||
import {ref, onMounted} from 'vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import {
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BFormSelect,
|
||||
BFormFile,
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
} from 'bootstrap-vue-next'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
let formFile = false
|
||||
const tree = ref(null)
|
||||
|
|
@ -209,11 +214,14 @@ const withParents = (tree) => {
|
|||
}
|
||||
|
||||
const isEditable = (item) => {
|
||||
return ['application/vnd.oasis.opendocument.spreadsheet', 'text/csv'].includes(item.type)
|
||||
return [
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'text/csv',
|
||||
].includes(item.type)
|
||||
}
|
||||
|
||||
const doAddFile = () => {
|
||||
const data = { file: null }
|
||||
const data = {file: null}
|
||||
formFile = true
|
||||
|
||||
form.value = {
|
||||
|
|
@ -237,7 +245,7 @@ const doAddFile = () => {
|
|||
}
|
||||
|
||||
const doAddDirectory = () => {
|
||||
const data = { directory: null, path: tree.value.id }
|
||||
const data = {directory: null, path: tree.value.id}
|
||||
formFile = false
|
||||
|
||||
form.value = {
|
||||
|
|
@ -264,13 +272,10 @@ const doDelete = (item) => {
|
|||
return
|
||||
}
|
||||
|
||||
fetch(
|
||||
`/api/filemanager/file/${item.id}`,
|
||||
createRequestOptions({
|
||||
method: 'DELETE',
|
||||
}),
|
||||
).then(() => {
|
||||
refresh({ current: tree.value.id })
|
||||
fetch(`/api/filemanager/file/${item.id}`, createRequestOptions({
|
||||
method: 'DELETE',
|
||||
})).then(() => {
|
||||
refresh({current: tree.value.id})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -282,16 +287,13 @@ const doSave = (e) => {
|
|||
payload.append('file', form.value.data.file)
|
||||
payload.append('path', tree.value.id)
|
||||
|
||||
fetch(
|
||||
`/api/filemanager/file`,
|
||||
createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: payload,
|
||||
}),
|
||||
)
|
||||
fetch(`/api/filemanager/file`, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: payload,
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
|
|
@ -301,24 +303,21 @@ const doSave = (e) => {
|
|||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh({ current: tree.value.id })
|
||||
refresh({current: tree.value.id})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
} else {
|
||||
fetch(
|
||||
`/api/filemanager/directory`,
|
||||
createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}),
|
||||
)
|
||||
fetch(`/api/filemanager/directory`, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
|
|
@ -328,7 +327,7 @@ const doSave = (e) => {
|
|||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh({ current: tree.value.id })
|
||||
refresh({current: tree.value.id})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -373,10 +372,3 @@ const searchTree = (id, tree) => {
|
|||
|
||||
onMounted(() => refresh())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
369
frontend/js/views/SavingAccountsView.vue
Normal file
369
frontend/js/views/SavingAccountsView.vue
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<Header title="Comptes épargnes">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<Pager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
|
||||
<div class="crud-list">
|
||||
<BTableSimple
|
||||
v-if="data !== null"
|
||||
caption-top
|
||||
responsive
|
||||
>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.thClasses"
|
||||
valign="top"
|
||||
@click="doSort(field.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="field.orderKey ?? field.key"
|
||||
:label="field.label ?? field.renderLabel(data.rows)"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="(row, key) in data.rows"
|
||||
:key="key"
|
||||
>
|
||||
<BTd
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="cursor"
|
||||
:class="field.tdClasses"
|
||||
@click="doEdit(row)"
|
||||
>
|
||||
<span v-if="field.key">
|
||||
<span
|
||||
v-if="field.render"
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[field.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</div>
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
v-model="formShow"
|
||||
:title="form?.label"
|
||||
footer-class="justify-content-between"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
</BFormGroup>
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="form.data.id"
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BTbody,
|
||||
BThead,
|
||||
BTr,
|
||||
BTd,
|
||||
BTh,
|
||||
BContainer,
|
||||
BTableSimple,
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BButtonToolbar,
|
||||
} from 'bootstrap-vue-next'
|
||||
|
||||
import SortButton from './../components/SortButton.vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import Pager from './../components/crud/Pager.vue'
|
||||
import {ref, onMounted, watch} from 'vue'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {renderDate, renderEuro, renderLabelWithSum} from '../lib/renderers'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
const endpoint = `/api/saving_account`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
fetch(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})}`,
|
||||
createRequestOptions()
|
||||
)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (value) {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
const data = {...item}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}/${item.id}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: data.label,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Montant bloqué',
|
||||
type: 'number',
|
||||
key: 'blocked_amount',
|
||||
},
|
||||
{
|
||||
label: 'Montant débloqué',
|
||||
type: 'number',
|
||||
key: 'released_amount',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
const data = {label: null, released_amount: 0, blocked_amount: 0}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: 'Nouveau',
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Libellé',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'label',
|
||||
},
|
||||
{
|
||||
label: 'Montant bloqué',
|
||||
type: 'number',
|
||||
key: 'blocked_amount',
|
||||
},
|
||||
{
|
||||
label: 'Montant débloqué',
|
||||
type: 'number',
|
||||
key: 'released_amount',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})).then(() => {
|
||||
formShow.value = false
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const url = form.value.data.id
|
||||
? `${endpoint}/${form.value.data.id}`
|
||||
: endpoint
|
||||
|
||||
form.value.data.released_amount = parseFloat(form.value.data.released_amount)
|
||||
form.value.data.blocked_amount = parseFloat(form.value.data.blocked_amount)
|
||||
|
||||
fetch(url, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = data.message
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
{
|
||||
key: 'blocked_amount',
|
||||
renderLabel: (rows) => renderLabelWithSum('Bloqué', rows, 'blocked_amount'),
|
||||
render: (item) => renderEuro(item.blocked_amount),
|
||||
width: '170px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
},
|
||||
{
|
||||
key: 'released_amount',
|
||||
renderLabel: (rows) =>
|
||||
renderLabelWithSum('Débloqué', rows, 'released_amount'),
|
||||
render: (item) => renderEuro(item.released_amount),
|
||||
width: '170px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
label: 'Mise à jour',
|
||||
render: (item) => renderDate(item.updated_at),
|
||||
width: '170px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
528
frontend/js/views/TransactionsView.vue
Normal file
528
frontend/js/views/TransactionsView.vue
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<Header title="Transactions">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="filtersShow = true"
|
||||
>Filtres</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Importer</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<Pager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
|
||||
<BModal
|
||||
v-if="filtersShow"
|
||||
v-model="filtersShow"
|
||||
title="Filtres"
|
||||
hide-footer
|
||||
>
|
||||
<Filters
|
||||
:data="filters"
|
||||
:fields="filtersFields"
|
||||
@update="doUpdateFilters"
|
||||
/>
|
||||
</BModal>
|
||||
<div class="crud-list">
|
||||
<BTableSimple
|
||||
v-if="data !== null"
|
||||
caption-top
|
||||
>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.thClasses"
|
||||
valign="top"
|
||||
@click="doSort(field.orderKey ?? field.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="field.orderKey ?? field.key"
|
||||
:label="field.label ?? field.renderLabel(data.rows)"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="(row, key) in data.rows"
|
||||
:key="key"
|
||||
>
|
||||
<BTd
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.tdClasses"
|
||||
@click="doInfo(row)"
|
||||
>
|
||||
<span v-if="field.key">
|
||||
<span
|
||||
v-if="field.render"
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[field.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</div>
|
||||
<BModal
|
||||
v-if="info !== null"
|
||||
v-model="infoShow"
|
||||
class="modal-lg info"
|
||||
:title="info.reference"
|
||||
>
|
||||
<BTableSimple
|
||||
responsive
|
||||
class="w-100"
|
||||
>
|
||||
<BTr>
|
||||
<BTh>Libellé</BTh>
|
||||
<BTd>{{ info.label }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Libellé simplifié</BTh>
|
||||
<BTd>{{ info.short_label }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Référence</BTh>
|
||||
<BTd>{{ info.reference }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Informations</BTh>
|
||||
<BTd>{{ info.information }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Type d'opération</BTh>
|
||||
<BTd>{{ info.operation_type }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Débit</BTh>
|
||||
<BTd>{{ info.debit }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Crédit</BTh>
|
||||
<BTd>{{ info.credit }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Date</BTh>
|
||||
<BTd>{{ renderDate(info.date) }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Comptabilisée le</BTh>
|
||||
<BTd>{{ renderDate(info.accounted_at) }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Catégorie banque</BTh><BTd>{{ info.bank_category }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Sous-catégorie banque</BTh>
|
||||
<BTd>{{ info.bank_sub_category }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Catégorie</BTh>
|
||||
<BTd v-html="renderCategory(info.category)"></BTd>
|
||||
</BTr>
|
||||
</BTableSimple>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="infoShow = false"
|
||||
>Fermer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
v-model="formShow"
|
||||
:title="form?.label"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<BFormFile
|
||||
v-if="field.widget === 'file'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:required="field.required"
|
||||
/>
|
||||
|
||||
<BFormSelect
|
||||
v-if="field.widget === 'select'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:options="field.options"
|
||||
:required="field.required"
|
||||
/>
|
||||
</BFormGroup>
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<div></div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BTbody,
|
||||
BThead,
|
||||
BTr,
|
||||
BTd,
|
||||
BTh,
|
||||
BContainer,
|
||||
BTableSimple,
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BButtonToolbar,
|
||||
BFormSelect,
|
||||
BFormFile,
|
||||
} from 'bootstrap-vue-next'
|
||||
|
||||
import SortButton from './../components/SortButton.vue'
|
||||
import Filters from './../components/Filters.vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import Pager from './../components/crud/Pager.vue'
|
||||
import {ref, onMounted, watch} from 'vue'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {
|
||||
renderDate,
|
||||
renderCategory,
|
||||
renderBankAccount,
|
||||
renderEuro,
|
||||
renderLabelWithSum,
|
||||
} from '../lib/renderers'
|
||||
import {queryFilters, appendRequestQueryFilters} from '../lib/filter'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
|
||||
const endpoint = `/api/transaction`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
const infoShow = ref(false)
|
||||
const info = ref(null)
|
||||
const filters = ref(getStorage(`${endpoint}:filters`) ?? [])
|
||||
const filtersShow = ref(false)
|
||||
let route = null
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v, 'order'))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v, 'sort'))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v, 'page'))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v, 'limit'))
|
||||
watch(filters, (v) => saveStorage(`${endpoint}:filters`, v, 'filters'))
|
||||
|
||||
const doInfo = (item) => {
|
||||
info.value = item
|
||||
infoShow.value = true
|
||||
}
|
||||
|
||||
const doUpdateFilters = (a) => {
|
||||
saveStorage(`${endpoint}:filters`, a, 'filters')
|
||||
filters.value = a
|
||||
refresh()
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
let query = {
|
||||
...queryFilters(filters.value),
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
}
|
||||
|
||||
query = appendRequestQueryFilters(query, route)
|
||||
|
||||
fetch(`${endpoint}?${new URLSearchParams(query)}`, createRequestOptions())
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (value) {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
const data = {category_id: null, file: null, format: 'caisse_epargne'}
|
||||
|
||||
fetch(`/api/bank_account?order=label`, createRequestOptions())
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((accounts) => {
|
||||
let options = []
|
||||
|
||||
accounts.rows.forEach((item) => {
|
||||
options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: 'Importer',
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Compte bancaire',
|
||||
widget: 'select',
|
||||
options: options,
|
||||
required: true,
|
||||
key: 'bank_account_id',
|
||||
},
|
||||
{
|
||||
label: 'Format de fichier',
|
||||
key: 'format',
|
||||
widget: 'select',
|
||||
options: [
|
||||
{value: 'caisse_epargne', text: "Caisse d'épargne"},
|
||||
{value: 'revolut', text: 'Revolut'},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Fichier',
|
||||
description: `Fichier CSV des opérations`,
|
||||
widget: 'file',
|
||||
required: true,
|
||||
key: 'file',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const payload = new FormData()
|
||||
payload.append('bank_account_id', form.value.data.bank_account_id)
|
||||
payload.append('file', form.value.data.file)
|
||||
payload.append('format', form.value.data.format)
|
||||
|
||||
fetch(`/api/transactions`, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: payload,
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = data.message
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const filtersFields = ref([
|
||||
{key: 'label', type: 'string', label: 'Libellé'},
|
||||
{key: 'debit', type: 'number', label: 'Débit'},
|
||||
{key: 'credit', type: 'number', label: 'Crédit'},
|
||||
{key: 'date', type: 'date', label: 'Date'},
|
||||
{key: 'operation_type', type: 'string', label: 'Type'},
|
||||
{key: 'category_id', type: 'select', label: 'Catégorie', options: []},
|
||||
{key: 'bank_account_id', type: 'select', label: 'Compte', options: []},
|
||||
])
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
width: '90px',
|
||||
render: (item) => renderDate(item.date),
|
||||
},
|
||||
{
|
||||
key: 'label',
|
||||
label: 'Libellé',
|
||||
},
|
||||
{
|
||||
key: 'operation_type',
|
||||
width: '200px',
|
||||
label: 'Type',
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
label: 'Catégorie',
|
||||
orderKey: 'category_id',
|
||||
width: '400px',
|
||||
render: (item) => renderCategory(item.category),
|
||||
},
|
||||
{
|
||||
key: 'bank_account',
|
||||
label: 'Compte',
|
||||
width: '200px',
|
||||
orderKey: 'bank_account_id',
|
||||
render: (item) => renderBankAccount(item.bank_account),
|
||||
},
|
||||
{
|
||||
key: 'debit',
|
||||
renderLabel: (rows) => renderLabelWithSum('Débit', rows, 'debit'),
|
||||
width: '120px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.debit),
|
||||
},
|
||||
{
|
||||
key: 'credit',
|
||||
renderLabel: (rows) => renderLabelWithSum('Crédit', rows, 'credit'),
|
||||
width: '120px',
|
||||
thClasses: ['text-end'],
|
||||
tdClasses: ['text-end'],
|
||||
render: (item) => renderEuro(item.credit),
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
route = useRoute()
|
||||
refresh()
|
||||
|
||||
fetch('/api/category', createRequestOptions())
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
filtersFields.value.forEach((value, key) => {
|
||||
if (value.key === 'category_id') {
|
||||
data.rows.forEach((item) => {
|
||||
filtersFields.value[key].options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
fetch('/api/bank_account', createRequestOptions())
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
filtersFields.value.forEach((value, key) => {
|
||||
if (value.key === 'bank_account_id') {
|
||||
data.rows.forEach((item) => {
|
||||
filtersFields.value[key].options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info th,
|
||||
.info td {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
352
frontend/js/views/UsersView.vue
Normal file
352
frontend/js/views/UsersView.vue
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<Header title="Utilisateurs">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<Pager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
|
||||
<div class="crud-list">
|
||||
<BTableSimple
|
||||
v-if="data !== null"
|
||||
caption-top
|
||||
responsive
|
||||
>
|
||||
<BThead>
|
||||
<BTr>
|
||||
<BTh
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
:width="field.width"
|
||||
class="cursor"
|
||||
:class="field.classes"
|
||||
@click="doSort(field.key)"
|
||||
>
|
||||
<SortButton
|
||||
:current-order="order"
|
||||
:current-sort="sort"
|
||||
:order="field.key"
|
||||
:label="field.label"
|
||||
/>
|
||||
</BTh>
|
||||
</BTr>
|
||||
</BThead>
|
||||
<BTbody>
|
||||
<BTr
|
||||
v-for="(row, key) in data.rows"
|
||||
:key="key"
|
||||
>
|
||||
<BTd
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="cursor"
|
||||
@click="doEdit(row)"
|
||||
>
|
||||
<span v-if="field.key">
|
||||
<span
|
||||
v-if="field.render"
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
<span
|
||||
v-else
|
||||
v-text="row[field.key]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-html="field.render(row)"
|
||||
></span>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTbody>
|
||||
</BTableSimple>
|
||||
</div>
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
v-model="formShow"
|
||||
:title="form?.label"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
v-text="form.error"
|
||||
></BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<BFormGroup
|
||||
v-for="(field, key) in form.fields"
|
||||
:id="'form-label-' + key"
|
||||
:key="key"
|
||||
class="mb-2"
|
||||
:label="field.label"
|
||||
:label-for="'form-label-' + key"
|
||||
:description="field.description"
|
||||
>
|
||||
<BFormInput
|
||||
v-if="(field.widget ?? 'text') === 'text'"
|
||||
:id="'form-input-' + key"
|
||||
v-model="form.data[field.key]"
|
||||
:type="field.type"
|
||||
:required="field.required"
|
||||
/>
|
||||
</BFormGroup>
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="form.data.id"
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BTbody,
|
||||
BThead,
|
||||
BTr,
|
||||
BTd,
|
||||
BTh,
|
||||
BContainer,
|
||||
BTableSimple,
|
||||
BModal,
|
||||
BButton,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BAlert,
|
||||
BButtonToolbar,
|
||||
} from 'bootstrap-vue-next'
|
||||
|
||||
import SortButton from './../components/SortButton.vue'
|
||||
import Header from './../components/crud/Header.vue'
|
||||
import Pager from './../components/crud/Pager.vue'
|
||||
import {ref, onMounted, watch} from 'vue'
|
||||
import {getStorage, saveStorage} from '../lib/storage'
|
||||
import {createRequestOptions} from '../lib/request'
|
||||
import {renderDateTime} from '../lib/renderers'
|
||||
|
||||
const endpoint = `/api/user`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
fetch(
|
||||
`${endpoint}?${new URLSearchParams({
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
})}`,
|
||||
createRequestOptions()
|
||||
)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(function (value) {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
const data = {...item}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}/${item.id}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: data.display_name,
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nom',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'display_name',
|
||||
},
|
||||
{
|
||||
label: "Nom d'utilisateur",
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
label: 'Mot de passe',
|
||||
type: 'password',
|
||||
required: false,
|
||||
key: 'password',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
const data = {display_name: null, username: null, password: null}
|
||||
|
||||
form.value = {
|
||||
action: `${endpoint}`,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
label: 'Nouveau',
|
||||
error: null,
|
||||
fields: [
|
||||
{
|
||||
label: 'Nom',
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'display_name',
|
||||
},
|
||||
{
|
||||
label: "Nom d'utilisateur",
|
||||
type: 'text',
|
||||
required: true,
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
label: 'Mot de passe',
|
||||
type: 'password',
|
||||
required: false,
|
||||
key: 'password',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formShow.value = true
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})).then(() => {
|
||||
formShow.value = false
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const url = form.value.data.id
|
||||
? `${endpoint}/${form.value.data.id}`
|
||||
: endpoint
|
||||
|
||||
fetch(url, createRequestOptions({
|
||||
method: form.value.method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value.data),
|
||||
}))
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = data.message
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
})
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
key: 'display_name',
|
||||
label: 'Nom',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Utilisateur',
|
||||
},
|
||||
{
|
||||
key: 'logged_at',
|
||||
label: 'Dernière connexion',
|
||||
render: (item) => renderDateTime(item.logged_at),
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
refresh({})
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
class="p-3"
|
||||
@submit="doSave"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { create, createForm } from '../../models/bank_account'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref(null)
|
||||
|
||||
const doSave = () => {
|
||||
create(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'bank_accounts' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
form.value = createForm()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="data === null"
|
||||
class="text-center p-5"
|
||||
>
|
||||
<p>Chargement...</p>
|
||||
<BSpinner />
|
||||
</div>
|
||||
<BContainer
|
||||
v-else
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="data.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
@submit="doSave"
|
||||
class="p-3"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { update, remove, getOne, createForm } from '../../models/bank_account'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const data = ref(null)
|
||||
const form = ref(null)
|
||||
const id = route.params.id
|
||||
|
||||
const loadData = () => {
|
||||
getOne(
|
||||
{
|
||||
id__eq: id,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
form.value = createForm(value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(id, doBack, doBack)
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
update(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'bank_accounts' })
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doCreate"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<CrudPager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
|
||||
<div
|
||||
v-if="data"
|
||||
class="crud-list"
|
||||
>
|
||||
<DataList
|
||||
:rows="data.rows"
|
||||
:headers="getListFields()"
|
||||
:sort="sort"
|
||||
:order="order"
|
||||
@sort="doSort"
|
||||
@row-click="doEdit"
|
||||
/>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import CrudPager from '../../components/crud/CrudPager.vue'
|
||||
import DataList from '../../components/crud/DataList.vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getList, getListFields } from '../../models/bank_account'
|
||||
|
||||
const endpoint = `/api/bank_account`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const router = useRouter()
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
getList(
|
||||
{
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
router.replace({ name: 'bank_account_edit', params: { id: item.id } })
|
||||
}
|
||||
|
||||
const doCreate = () => {
|
||||
router.replace({ name: 'bank_account_create' })
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
class="p-3"
|
||||
@submit="submit"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { create, createForm } from '../../models/category'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref(null)
|
||||
|
||||
const doSave = () => {
|
||||
create(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
router.replace({ name: 'category_edit', params: { id: data.id } })
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'categories' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
form.value = createForm()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="data === null"
|
||||
class="text-center p-5"
|
||||
>
|
||||
<p>Chargement...</p>
|
||||
<BSpinner />
|
||||
</div>
|
||||
<BContainer
|
||||
v-else
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="data.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doApply"
|
||||
>
|
||||
<VueSpinner
|
||||
v-if="applyInProgress"
|
||||
size="20"
|
||||
color="white"
|
||||
/>
|
||||
<span v-else>Appliquer les règles</span>
|
||||
</BButton>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="form.data['rules'] = doAddRule(form.data['rules'])"
|
||||
>Ajouter règle</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
@submit="doSave"
|
||||
class="p-3"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol
|
||||
cols="12"
|
||||
:md="form.data.rules && form.data.rules.length > 0 ? 4 : 12"
|
||||
>
|
||||
<FormView
|
||||
:form="form"
|
||||
:fields="['label', 'month_threshold', 'ignore_transactions', 'color']"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol
|
||||
cols="12"
|
||||
:md="form.data.rules && form.data.rules.length > 0 ? 8 : 12"
|
||||
>
|
||||
<div
|
||||
class="rounded"
|
||||
:class="{ border: form.data.rules && form.data.rules.length > 0 }"
|
||||
>
|
||||
<FormView
|
||||
:form="form"
|
||||
:fields="['rules']"
|
||||
/>
|
||||
</div>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { VueSpinner } from 'vue3-spinners'
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { update, remove, getOne, createForm } from '../../models/category'
|
||||
import { updateCategories, getOne as getTransactionOne } from '../../models/transaction'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const data = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
const applyInProgress = ref(false)
|
||||
const id = route.params.id
|
||||
const transactionId = route.params.transaction
|
||||
let addTransaction = transactionId !== ''
|
||||
|
||||
const loadData = () => {
|
||||
getOne(
|
||||
{
|
||||
id__eq: id,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
form.value = createForm(value)
|
||||
checkTransaction()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const checkTransaction = () => {
|
||||
if (!addTransaction) {
|
||||
return
|
||||
}
|
||||
|
||||
getTransactionOne(
|
||||
{
|
||||
id__eq: transactionId,
|
||||
},
|
||||
(transaction) => {
|
||||
doAddRule(form.value.data['rules'], {
|
||||
contain: transaction.reference,
|
||||
amount: null,
|
||||
match: null,
|
||||
bank_category: null,
|
||||
date_from: null,
|
||||
date_to: null,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doApply = () => {
|
||||
applyInProgress.value = true
|
||||
|
||||
const callback = () => {
|
||||
applyInProgress.value = false
|
||||
}
|
||||
|
||||
updateCategories(callback, callback)
|
||||
}
|
||||
|
||||
const doAddRule = (item, rule) => {
|
||||
if (!rule) {
|
||||
rule = {
|
||||
contain: null,
|
||||
match: null,
|
||||
bank_category: null,
|
||||
amount: null,
|
||||
date_from: null,
|
||||
date_to: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
item = []
|
||||
}
|
||||
|
||||
item.push(rule)
|
||||
|
||||
window.setTimeout(() => {
|
||||
const container = document.querySelector('#rules > div')
|
||||
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' })
|
||||
}, 300)
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(id, doBack, doBack)
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
update(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
form.value = null
|
||||
addTransaction = false
|
||||
formShow.value = false
|
||||
loadData()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
if (transactionId) {
|
||||
router.replace({ name: 'transactions' })
|
||||
} else {
|
||||
router.replace({ name: 'categories' })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#rules > div {
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="doApply"
|
||||
>
|
||||
<VueSpinner
|
||||
v-if="applyInProgress"
|
||||
size="20"
|
||||
color="white"
|
||||
/>
|
||||
<span v-else>Appliquer les règles</span>
|
||||
</BButton>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doCreate"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<CrudPager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
|
||||
<div
|
||||
v-if="data"
|
||||
class="crud-list"
|
||||
>
|
||||
<DataList
|
||||
:rows="data.rows"
|
||||
:headers="getListFields()"
|
||||
:sort="sort"
|
||||
:order="order"
|
||||
@sort="doSort"
|
||||
@row-click="doEdit"
|
||||
/>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
|
||||
|
||||
import { VueSpinner } from 'vue3-spinners'
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import CrudPager from '../../components/crud/CrudPager.vue'
|
||||
import DataList from '../../components/crud/DataList.vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getList, getListFields } from '../../models/category'
|
||||
import { updateCategories } from '../../models/transaction'
|
||||
|
||||
const endpoint = `/api/category`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const applyInProgress = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const doApply = () => {
|
||||
applyInProgress.value = true
|
||||
|
||||
const callback = () => {
|
||||
applyInProgress.value = false
|
||||
}
|
||||
|
||||
updateCategories(callback, callback)
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
getList(
|
||||
{
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
router.replace({ name: 'category_edit', params: { id: item.id } })
|
||||
}
|
||||
|
||||
const doCreate = () => {
|
||||
router.replace({ name: 'category_create' })
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
class="p-3"
|
||||
@submit="submit"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { create, createForm } from '../../models/saving_account'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref(null)
|
||||
|
||||
const doSave = () => {
|
||||
create(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'saving_accounts' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
form.value = createForm()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="data === null"
|
||||
class="text-center p-5"
|
||||
>
|
||||
<p>Chargement...</p>
|
||||
<BSpinner />
|
||||
</div>
|
||||
<BContainer
|
||||
v-else
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="data.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
@submit="doSave"
|
||||
class="p-3"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { update, remove, getOne, createForm } from '../../models/saving_account'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const data = ref(null)
|
||||
const form = ref(null)
|
||||
const id = route.params.id
|
||||
|
||||
const loadData = () => {
|
||||
getOne(
|
||||
{
|
||||
id__eq: id,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
form.value = createForm(value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(id, doBack, doBack)
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
update(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'saving_accounts' })
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doCreate"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<CrudPager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
|
||||
<div
|
||||
v-if="data"
|
||||
class="crud-list"
|
||||
>
|
||||
<DataList
|
||||
:rows="data.rows"
|
||||
:headers="getListFields()"
|
||||
:sort="sort"
|
||||
:order="order"
|
||||
@sort="doSort"
|
||||
@row-click="doEdit"
|
||||
/>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import CrudPager from '../../components/crud/CrudPager.vue'
|
||||
import DataList from '../../components/crud/DataList.vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getList, getListFields } from '../../models/saving_account'
|
||||
|
||||
const endpoint = `/api/saving_account`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const router = useRouter()
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
getList(
|
||||
{
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
router.replace({ name: 'saving_account_edit', params: { id: item.id } })
|
||||
}
|
||||
|
||||
const doCreate = () => {
|
||||
router.replace({ name: 'saving_account_create' })
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<RouterView />
|
||||
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="filtersShow = true"
|
||||
>Filtres</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doAdd"
|
||||
>Importer</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<CrudPager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
|
||||
<div
|
||||
v-if="data"
|
||||
class="crud-list"
|
||||
>
|
||||
<DataList
|
||||
:rows="data.rows"
|
||||
:headers="getListFields()"
|
||||
:sort="sort"
|
||||
:order="order"
|
||||
@sort="doSort"
|
||||
@row-click="doShow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BModal
|
||||
v-if="form !== null"
|
||||
v-model="formShow"
|
||||
:title="form?.label"
|
||||
@ok="doSave"
|
||||
>
|
||||
<BAlert
|
||||
:model-value="form.error !== null"
|
||||
variant="danger"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BForm @submit="doSave">
|
||||
<FormView :form="form" />
|
||||
</BForm>
|
||||
<template #footer>
|
||||
<div></div>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="me-2"
|
||||
@click="formShow = false"
|
||||
>Annuler</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>OK</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</BModal>
|
||||
|
||||
<BModal
|
||||
v-if="filtersShow"
|
||||
v-model="filtersShow"
|
||||
title="Filtres"
|
||||
hide-footer
|
||||
>
|
||||
<CrudFilter
|
||||
:data="filters"
|
||||
:fields="filtersFields"
|
||||
@update="doUpdateFilters"
|
||||
/>
|
||||
</BModal>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BModal, BButton, BForm, BAlert, BButtonToolbar } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import CrudFilter from '../../components/CrudFilter.vue'
|
||||
import CrudPager from '../../components/crud/CrudPager.vue'
|
||||
import FormView from '../../components/crud/FormView.vue'
|
||||
import DataList from '../../components/crud/DataList.vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { queryFilters, appendRequestQueryFilters } from '../../lib/filter'
|
||||
import { useRouter, RouterView } from 'vue-router'
|
||||
import { getList, getListFields, getListFilters, createForm, importFile } from '../../models/transaction'
|
||||
import { getList as getAccountList } from '../../models/bank_account'
|
||||
import { getList as getCategoryList } from '../../models/category'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const endpoint = `/api/transaction`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const form = ref(null)
|
||||
const formShow = ref(false)
|
||||
const filters = ref(getStorage(`${endpoint}:filters`) ?? [])
|
||||
const filtersShow = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
let query = {
|
||||
...queryFilters(filters.value),
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
}
|
||||
|
||||
query = appendRequestQueryFilters(query, endpoint)
|
||||
|
||||
getList(query, (value) => {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
})
|
||||
}
|
||||
|
||||
const doShow = (item) => {
|
||||
router.replace({ name: 'transaction_show', params: { id: item.id } })
|
||||
}
|
||||
|
||||
const doUpdateFilters = (a) => {
|
||||
saveStorage(`${endpoint}:filters`, a, 'filters')
|
||||
filters.value = a
|
||||
refresh()
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
const doAdd = () => {
|
||||
getAccountList({}, (accounts) => {
|
||||
form.value = createForm(accounts.rows)
|
||||
formShow.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
importFile(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
form.value = null
|
||||
formShow.value = false
|
||||
refresh()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const filtersFields = ref(getListFilters())
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
|
||||
getCategoryList({}, (data) => {
|
||||
filtersFields.value.forEach((value, key) => {
|
||||
if (value.key === 'category_id') {
|
||||
data.rows.forEach((item) => {
|
||||
filtersFields.value[key].options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
getAccountList({}, (data) => {
|
||||
filtersFields.value.forEach((value, key) => {
|
||||
if (value.key === 'bank_account_id') {
|
||||
data.rows.forEach((item) => {
|
||||
filtersFields.value[key].options.push({
|
||||
value: item.id,
|
||||
text: item.label,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<template>
|
||||
<div v-if="data">
|
||||
<BModal
|
||||
id="transaction"
|
||||
v-model="modal"
|
||||
scrollable
|
||||
ok-only
|
||||
size="lg"
|
||||
:title="data.label"
|
||||
@hide="onClose"
|
||||
>
|
||||
<BTableSimple
|
||||
v-if="data"
|
||||
responsive
|
||||
class="table"
|
||||
>
|
||||
<BTr>
|
||||
<BTh width="30%">Libellé</BTh>
|
||||
<BTd>{{ data.label }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Libellé simplifié</BTh>
|
||||
<BTd>{{ data.short_label }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Référence</BTh>
|
||||
<BTd>{{ data.reference }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Informations</BTh>
|
||||
<BTd>{{ data.transactionrmation }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Type d'opération</BTh>
|
||||
<BTd>{{ data.operation_type }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Débit</BTh>
|
||||
<BTd>{{ data.debit }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Crédit</BTh>
|
||||
<BTd>{{ data.credit }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Date</BTh>
|
||||
<BTd>{{ renderDate(data.date) }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Comptabilisée le</BTh>
|
||||
<BTd>{{ renderDate(data.accounted_at) }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Catégorie banque</BTh>
|
||||
<BTd>{{ data.bank_category }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh>Sous-catégorie banque</BTh>
|
||||
<BTd>{{ data.bank_sub_category }}</BTd>
|
||||
</BTr>
|
||||
<BTr>
|
||||
<BTh class="border-0">Catégorie</BTh>
|
||||
<BTd class="border-0">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="data.category"
|
||||
class="p-0"
|
||||
v-html="renderCategory(data.category)"
|
||||
></span>
|
||||
<BRow
|
||||
v-else
|
||||
class="m-0 p-0"
|
||||
>
|
||||
<BCol class="ps-0">
|
||||
<BFormSelect
|
||||
v-model="category"
|
||||
:options="categories"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<BButton
|
||||
v-if="category"
|
||||
variant="primary"
|
||||
@click="createRule"
|
||||
>Créer règle</BButton
|
||||
>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BTd>
|
||||
</BTr>
|
||||
</BTableSimple>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BTableSimple, BTr, BTh, BTd, BModal, BFormSelect, BButton, BRow, BCol } from 'bootstrap-vue-next'
|
||||
import { renderDate, renderCategory } from '../../lib/renderers'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getOne } from '../../models/transaction'
|
||||
import { getList as getCategoryList } from '../../models/category'
|
||||
|
||||
const data = ref(null)
|
||||
const modal = ref(true)
|
||||
const router = useRouter()
|
||||
const categories = ref([])
|
||||
const category = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const id = ref(props.id)
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
(newValue) => {
|
||||
id.value = newValue
|
||||
loadData()
|
||||
},
|
||||
)
|
||||
|
||||
const loadData = () => {
|
||||
getOne(
|
||||
{
|
||||
id__eq: id.value,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
modal.value = true
|
||||
},
|
||||
)
|
||||
|
||||
getCategoryList({}, (data) => {
|
||||
categories.value = []
|
||||
data.rows.forEach((row) => {
|
||||
categories.value.push({
|
||||
value: row.id,
|
||||
text: row.label,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
router.replace({ name: 'transactions' })
|
||||
}
|
||||
|
||||
const createRule = () => {
|
||||
router.replace({ name: 'category_edit', params: { id: category.value, transaction: data.value.id } })
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
class="p-3"
|
||||
@submit="submit"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import FormView from '../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { create, createForm } from '../../models/user'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const router = useRouter()
|
||||
const parentRouteName = 'users'
|
||||
const form = ref(null)
|
||||
|
||||
const doSave = () => {
|
||||
create(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: parentRouteName })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
form.value = createForm()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="data === null"
|
||||
class="text-center p-5"
|
||||
>
|
||||
<p>Chargement...</p>
|
||||
<BSpinner />
|
||||
</div>
|
||||
<BContainer
|
||||
v-else
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="data.display_name">
|
||||
<template #menu>
|
||||
<div>
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="doBack"
|
||||
>Retour</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doSave"
|
||||
>Enregistrer</BButton
|
||||
>
|
||||
<BButton
|
||||
variant="danger"
|
||||
@click="doDelete"
|
||||
>Supprimer</BButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #bottom> </template>
|
||||
</CrudHeader>
|
||||
|
||||
<BForm
|
||||
v-if="form !== null"
|
||||
@submit="doSave"
|
||||
class="p-3"
|
||||
>
|
||||
<BAlert
|
||||
v-if="form?.error"
|
||||
dismissible
|
||||
variant="warning"
|
||||
:model-value="true"
|
||||
>
|
||||
<div v-html="form.error"></div>
|
||||
</BAlert>
|
||||
<BRow>
|
||||
<BCol>
|
||||
<FormView :form="form" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BForm>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BForm, BRow, BCol, BAlert } from 'bootstrap-vue-next'
|
||||
import CrudHeader from './../../components/crud/CrudHeader.vue'
|
||||
import FormView from './../../components/crud/FormView.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { update, remove, getOne, createForm } from '../../models/user'
|
||||
import { requestErrorBuilder } from '../../lib/request'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const data = ref(null)
|
||||
const form = ref(null)
|
||||
const id = route.params.id
|
||||
|
||||
const loadData = () => {
|
||||
getOne(
|
||||
{
|
||||
id__eq: id,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
form.value = createForm(value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doDelete = () => {
|
||||
if (!confirm('Je confirme la suppression')) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(id, doBack, doBack)
|
||||
}
|
||||
|
||||
const doSave = () => {
|
||||
update(
|
||||
form.value.data,
|
||||
(data) => {
|
||||
if (data.code === 400) {
|
||||
form.value.error = requestErrorBuilder(data)
|
||||
} else {
|
||||
doBack()
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
form.value.error = `Une erreur s'est produite : ${err}`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doBack = () => {
|
||||
router.replace({ name: 'users' })
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
<template>
|
||||
<BContainer
|
||||
fluid
|
||||
class="p-0"
|
||||
>
|
||||
<CrudHeader :title="$route.meta.label">
|
||||
<template #menu>
|
||||
<BButtonToolbar key-nav>
|
||||
<BButton
|
||||
variant="primary"
|
||||
@click="doCreate"
|
||||
>Ajouter</BButton
|
||||
>
|
||||
</BButtonToolbar>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<CrudPager
|
||||
v-model:page="page"
|
||||
v-model:pages="pages"
|
||||
v-model:limit="limit"
|
||||
@update="refresh()"
|
||||
/>
|
||||
</template>
|
||||
</CrudHeader>
|
||||
|
||||
<div
|
||||
v-if="data"
|
||||
class="crud-list"
|
||||
>
|
||||
<DataList
|
||||
:rows="data.rows"
|
||||
:headers="getListFields()"
|
||||
:sort="sort"
|
||||
:order="order"
|
||||
@sort="doSort"
|
||||
@row-click="doEdit"
|
||||
/>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
|
||||
|
||||
import CrudHeader from '../../components/crud/CrudHeader.vue'
|
||||
import CrudPager from '../../components/crud/CrudPager.vue'
|
||||
import DataList from '../../components/crud/DataList.vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { getStorage, saveStorage } from '../../lib/storage'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getList, getListFields } from '../../models/user'
|
||||
|
||||
const endpoint = `/api/user`
|
||||
const order = ref(getStorage(`${endpoint}:order`))
|
||||
const sort = ref(getStorage(`${endpoint}:sort`))
|
||||
const page = ref(getStorage(`${endpoint}:page`))
|
||||
const limit = ref(getStorage(`${endpoint}:limit`))
|
||||
const data = ref(null)
|
||||
const pages = ref(null)
|
||||
const router = useRouter()
|
||||
|
||||
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
|
||||
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
|
||||
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
|
||||
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
|
||||
|
||||
const refresh = () => {
|
||||
getList(
|
||||
{
|
||||
order: order.value,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
},
|
||||
(value) => {
|
||||
data.value = value
|
||||
order.value = value.order
|
||||
sort.value = value.sort
|
||||
page.value = value.page
|
||||
pages.value = value.total_pages
|
||||
limit.value = value.limit
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const doEdit = (item) => {
|
||||
router.replace({ name: 'user_edit', params: { id: item.id } })
|
||||
}
|
||||
|
||||
const doCreate = () => {
|
||||
router.replace({ name: 'user_create' })
|
||||
}
|
||||
|
||||
const doSort = (key) => {
|
||||
let nextSort = 'asc'
|
||||
|
||||
if (order.value === key) {
|
||||
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
order.value = key
|
||||
sort.value = nextSort
|
||||
page.value = 1
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
$white: #ffffff;
|
||||
|
||||
$theme-colors: (
|
||||
'navBg': #f5f6f7,
|
||||
'navText': #47516b,
|
||||
'navActiveBg': #3072c7,
|
||||
'light': #f1c3a2,
|
||||
'light': #bfecf8,
|
||||
'dark': #07070b,
|
||||
'primary': #3072c7,
|
||||
'secondary': #a2c7e7,
|
||||
'header': #d6c52e,
|
||||
'primary': #293e5b,
|
||||
'secondary': #4e76ac,
|
||||
'info': #abca41,
|
||||
'success': #1eaa62,
|
||||
'warning': #e5cc1d,
|
||||
|
|
@ -16,31 +12,36 @@ $theme-colors: (
|
|||
);
|
||||
|
||||
$pagination-active-bg: map-get($theme-colors, 'secondary');
|
||||
$nav-pills-link-active-bg: map-get($theme-colors, 'navActiveBg');
|
||||
$nav-pills-link-active-bg: darken(map-get($theme-colors, 'primary'), 5%);
|
||||
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import '~bootstrap-vue-next/dist/bootstrap-vue-next.css';
|
||||
@import '~@fortawesome/fontawesome-free/css/all.css';
|
||||
@import '@fontsource/ubuntu/300.css';
|
||||
|
||||
body {
|
||||
font-family: 'Ubuntu';
|
||||
}
|
||||
|
||||
$light-grey: #f5f6f7;
|
||||
$light-grey: #e9ecef;
|
||||
|
||||
#app-name {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
background: linear-gradient(-45deg, #f1f1f1 50%, #e1e1e1 100%);
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
map-get($theme-colors, 'primary') 50%,
|
||||
map-get($theme-colors, 'secondary') 100%
|
||||
);
|
||||
|
||||
.card {
|
||||
background: linear-gradient(-45deg, #f1f1f1 50%, #e1e1e1 100%);
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
darken(map-get($theme-colors, 'primary'), 10%) 50%,
|
||||
map-get($theme-colors, 'secondary') 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
color: #47516b;
|
||||
* {
|
||||
overscroll-behavior: contain !important;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
|
|
@ -59,11 +60,7 @@ $nav-size-sm: 50px;
|
|||
|
||||
#nav {
|
||||
width: $nav-size;
|
||||
background: map-get($theme-colors, 'navBg');
|
||||
|
||||
a:not(.active) {
|
||||
color: map-get($theme-colors, 'navText');
|
||||
}
|
||||
background: map-get($theme-colors, 'primary');
|
||||
|
||||
i {
|
||||
width: 20px;
|
||||
|
|
@ -80,7 +77,7 @@ $nav-size-sm: 50px;
|
|||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
canvas {
|
||||
|
|
@ -95,30 +92,17 @@ $nav-size-sm: 50px;
|
|||
|
||||
.crud {
|
||||
&-list {
|
||||
width: 100%;
|
||||
width: calc(100vw - $nav-size - 30px);
|
||||
padding: 20px;
|
||||
background: $light-grey;
|
||||
border-radius: 10px;
|
||||
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
tr {
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:hover td {
|
||||
background: $light-grey;
|
||||
}
|
||||
background: $light-grey;
|
||||
}
|
||||
|
||||
thead th {
|
||||
|
|
@ -129,27 +113,6 @@ $nav-size-sm: 50px;
|
|||
}
|
||||
}
|
||||
|
||||
#transaction {
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding-top: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
th {
|
||||
padding-left: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#body {
|
||||
width: calc(100vw - $nav-size);
|
||||
max-height: 100vh;
|
||||
|
|
@ -208,7 +171,7 @@ $nav-size-sm: 50px;
|
|||
|
||||
.crud {
|
||||
&-list {
|
||||
width: calc(100vw - $nav-size-sm);
|
||||
width: calc(100vw - $nav-size-sm - 30px);
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
|
@ -217,9 +180,3 @@ $nav-size-sm: 50px;
|
|||
width: calc(100vw - $nav-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-image: linear-gradient(45deg, #fafafa 25%, #f5f6f7 25%, #f5f6f7 50%, #fafafa 50%, #fafafa 75%, #f5f6f7 75%, #f5f6f7 100%);
|
||||
background-size: 56.57px 56.57px;
|
||||
color: #47516b;
|
||||
}
|
||||
|
|
|
|||
32
go.mod
32
go.mod
|
|
@ -5,7 +5,7 @@ go 1.23.0
|
|||
require (
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/urfave/cli/v2 v2.27.4
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
|
|
@ -14,11 +14,19 @@ require (
|
|||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/GeertJohan/go.rice v1.0.3 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/a-h/templ v0.2.778 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator v9.31.0+incompatible // indirect
|
||||
|
|
@ -30,19 +38,27 @@ require (
|
|||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/echo-contrib v0.17.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.12.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/swaggo/echo-swagger v1.4.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/swaggo/swag v1.16.4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
46
go.sum
46
go.sum
|
|
@ -3,6 +3,12 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
|
|||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
|
||||
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
|
||||
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
|
||||
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
|
|
@ -23,6 +29,16 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq
|
|||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
|
|
@ -47,18 +63,27 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU=
|
||||
github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA=
|
||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
|
|
@ -76,6 +101,13 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
|||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
|
||||
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
|
||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
|
|
@ -87,22 +119,36 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
|
|||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
4577
package-lock.json
generated
4577
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@fontsource/ubuntu": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@symfony/webpack-encore": "github:symfony/webpack-encore",
|
||||
"bootstrap": "^5.3.3",
|
||||
|
|
@ -15,6 +14,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^9.19.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue