package project import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "time" "management/internal/db/model/dto" formDto "management/internal/db/model/form" "management/internal/db/model/view" db "management/internal/db/sqlc" "management/internal/global" "management/internal/global/html" "management/internal/middleware/manage/auth" "management/internal/pkg/convertor" "management/internal/pkg/snowflake" "management/internal/router/manage/util" projectservice "management/internal/service/project" "management/internal/tpl" "github.com/jackc/pgx/v5/pgtype" ) func List(w http.ResponseWriter, r *http.Request) { tpl.HTML(w, r, "project/list.tmpl", map[string]any{ "Statuses": html.NewSelectControls(global.ProjectStatuses, 0), }) } func PostList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() title := strings.TrimSpace(r.PostFormValue("title")) var search string if len(title) > 0 { search = "%" + title + "%" if strings.HasSuffix(title, ":") { search = title[:len(title)-1] + "%" } } arg := &db.ListProjectConditionParam{ IsTitle: len(search) > 0, Title: search, Status: util.ConvertInt16(r.PostFormValue("status"), 9999), PageID: util.ConvertInt32(r.PostFormValue("page"), 1), PageSize: util.ConvertInt32(r.PostFormValue("rows"), 10), } arg.TimeBegin, arg.TimeEnd = util.DefaultStartTimeAndEndTime(r.PostFormValue("timeBegin"), r.PostFormValue("timeEnd")) res, total, err := db.Engine.ListProjectCondition(ctx, arg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := tpl.ResponseList{ Code: 0, Message: "ok", Count: total, Data: res, } tpl.JSON(w, data) } func Add(w http.ResponseWriter, r *http.Request) { authUser := auth.AuthUser(r.Context()) tpl.HTML(w, r, "project/edit.tmpl", map[string]any{ "Item": &formDto.ProjectForm{ ApplyUserID: authUser.ID, ProjectFiles: &formDto.ProjectFileForm{ ProjectFileItems: []*formDto.ProjectFileItemForm{}, }, }, "Statuses": html.NewSelectControls(global.ProjectStatuses, 0), }) } func Edit(w http.ResponseWriter, r *http.Request) { form := &formDto.ProjectForm{} id := convertor.ConvertInt[int64](r.URL.Query().Get("id"), 0) if id > 0 { ctx := r.Context() if po, err := db.Engine.GetProject(ctx, id); err == nil { pfs, err := db.Engine.ListProjectFiles(ctx, po.ID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } form = form.ToForm(po, pfs) if form.ApplyUserID == 0 { authUser := auth.AuthUser(ctx) form.ApplyUserID = authUser.ID } if c, err := db.Engine.GetCustomer(ctx, po.CustomerID); err == nil { form.CustomerName = c.Name } // if u, err := db.Engine.GetSysUser(ctx, po.ManagerID); err == nil { // form.ManagerName = u.Username // } // if len(po.Members) > 0 { // arr := strings.Split(po.Members, ",") // if len(arr) > 0 { // var ids []int32 // for _, v := range arr { // ids = append(ids, convertor.ConvertInt[int32](v, 0)) // } // if rows, err := db.Engine.ListSysUserByIds(ctx, ids); err == nil && len(rows) > 0 { // log.Println("rows: ", rows) // var names []string // for _, v := range rows { // names = append(names, v.Username) // } // form.MembersName = strings.Join(names, ",") // } // } // } // if u, err := db.Engine.GetSysUser(ctx, po.ApplyUserID); err == nil { // form.ApplyUserName = u.Username // } if u, err := db.Engine.GetSysUser(ctx, po.CreatedUserID); err == nil { form.CreatedName = u.Username } if u, err := db.Engine.GetSysUser(ctx, po.UpdatedUserID); err == nil { form.UpdatedName = u.Username } } } tpl.HTML(w, r, "project/edit.tmpl", map[string]any{ "Item": form, "Statuses": html.NewSelectControls(global.ProjectStatuses, form.Status), }) } func Save(w http.ResponseWriter, r *http.Request) { ctx := r.Context() form, err := validForm(r) if err != nil { tpl.JSONERR(w, err.Error()) return } authUser := auth.AuthUser(ctx) if form.ID > 0 { p := &db.UpdateProjectParams{ ID: form.ID, Name: pgtype.Text{ String: form.Name, Valid: true, }, StartAt: pgtype.Timestamptz{ Time: form.StartAt, Valid: true, }, EndAt: pgtype.Timestamptz{ Time: form.EndAt, Valid: true, }, CustomerID: pgtype.Int8{ Int64: form.CustomerID, Valid: true, }, TotalMoney: form.TotalMoneyF, Description: pgtype.Text{ String: form.Description, Valid: true, }, ApplyAt: pgtype.Timestamptz{ Time: form.ApplyAt, Valid: true, }, ApplyUserID: pgtype.Int4{ Int32: form.ApplyUserID, Valid: true, }, ManagerID: pgtype.Int4{ Int32: form.ManagerID, Valid: true, }, Members: pgtype.Text{ String: form.Members, Valid: true, }, Status: pgtype.Int2{ Int16: form.Status, Valid: true, }, UpdatedUserID: pgtype.Int4{ Int32: authUser.ID, Valid: true, }, } cpfs := []*db.CreateProjectFileParams{} for _, pfile := range form.ProjectFiles.ProjectFileItems { cpfs = append(cpfs, &db.CreateProjectFileParams{ ID: snowflake.GetId(), Name: pfile.Name, Path: pfile.Path, ProjectID: form.ID, CreatedUserID: authUser.ID, }) } err := projectservice.UpdateProject(ctx, p, cpfs) if err != nil { tpl.JSONERR(w, err.Error()) return } tpl.JSONOK(w, "更新成功") } else { p := &db.CreateProjectParams{ ID: snowflake.GetId(), Name: form.Name, StartAt: form.StartAt, EndAt: form.EndAt, CustomerID: form.CustomerID, TotalMoney: form.TotalMoneyF, Description: form.Description, ApplyAt: form.ApplyAt, ApplyUserID: form.ApplyUserID, ManagerID: form.ManagerID, Members: form.Members, Status: form.Status, CreatedUserID: authUser.ID, } cpfs := []*db.CreateProjectFileParams{} for _, pfile := range form.ProjectFiles.ProjectFileItems { cpfs = append(cpfs, &db.CreateProjectFileParams{ ID: snowflake.GetId(), Name: pfile.Name, Path: pfile.Path, ProjectID: p.ID, CreatedUserID: authUser.ID, }) } err := projectservice.CreateProject(ctx, p, cpfs) if err != nil { tpl.JSONERR(w, err.Error()) return } tpl.JSONOK(w, "添加成功") } } func XmSelect(w http.ResponseWriter, r *http.Request) { all, err := db.Engine.AllProjects(r.Context()) if err != nil { tpl.JSONERR(w, err.Error()) return } var res []*dto.XmSelectStrDto for _, v := range all { res = append(res, &dto.XmSelectStrDto{ Name: v.Name, Value: strconv.FormatInt(v.ID, 10), }) } tpl.JSON(w, res) } func Dashboard(w http.ResponseWriter, r *http.Request) { tpl.HTML(w, r, "project/dashboard.tmpl", map[string]any{ "Projects": projectservice.AllProjects(r.Context()), }) } func PostDashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() t := r.PostFormValue("type") if t == "project" { var projectIncome float64 var projectExpense float64 projectID := convertor.ConvertInt[int64](r.PostFormValue("projectID"), 0) if projectID == 0 { si, _ := db.Engine.SumIncome(ctx) projectIncome = convertor.NumericToFloat64(si) se, _ := db.Engine.SumExpense(ctx) projectExpense = convertor.NumericToFloat64(se) } else { pi, _ := db.Engine.SumIncomeByProjectID(ctx, projectID) projectIncome = convertor.NumericToFloat64(pi) pe, _ := db.Engine.SumExpenseByProjectID(ctx, projectID) projectExpense = convertor.NumericToFloat64(pe) } projectProfit := projectIncome - projectExpense var projectProfitRate float64 = 0 if projectExpense > 0 { projectProfitRate = projectProfit / projectExpense } res := &view.DashboardProject{ ProjectIncome: projectIncome, ProjectExpense: projectExpense, ProjectProfit: projectProfit, ProjectProfitRate: fmt.Sprintf("%0.2f%%", projectProfitRate*100), IncomeExpenseEcharts: incomeExpenseEcharts(ctx, projectID), IncomeEcharts: incomeEcharts(ctx, projectID), ExpenseEcharts: expenseEcharts(ctx, projectID), } tpl.JSON(w, tpl.Response{Success: true, Message: "ok", Data: res}) return } else if t == "budget" { } tpl.JSONERR(w, "failed to valid type") } func validForm(r *http.Request) (formDto.ProjectForm, error) { // data := &form.ProjectForm{} // if err := form.BindForm(r, data); err != nil { // return data, err // } // if err := data.TotalMoneyF.Scan(data.TotalMoney); err != nil { // return data, errors.New("总金额格式错误") // } // if len(data.Members) > 0 { // membersSplit := strings.SplitSeq(data.Members, ",") // for v := range membersSplit { // m := convertor.ConvertInt[int32](v, 0) // if m == 0 { // return data, errors.New("项目成员数据错误") // } // } // } // data.ProjectFiles = &formDto.ProjectFileForm{ // ProjectFileItems: []*formDto.ProjectFileItemForm{}, // } // if len(data.Paths) > 0 { // fnsSplit := strings.SplitSeq(data.Paths, ",") // for v := range fnsSplit { // if len(v) > 0 { // read := strings.Split(v, "|") // if len(read) != 2 { // return data, errors.New("文件路径数据错误") // } // pff := &formDto.ProjectFileItemForm{ // Name: read[0], // Path: read[1], // Combination: v, // } // data.ProjectFiles.ProjectFileItems = append(data.ProjectFiles.ProjectFileItems, pff) // } // } // } // return data, nil var err error form := formDto.ProjectForm{} form.ID = convertor.ConvertInt[int64](r.PostFormValue("ID"), 0) form.CustomerID, err = strconv.ParseInt(r.PostFormValue("CustomerID"), 10, 64) if err != nil || form.CustomerID == 0 { return form, errors.New("客户不能为空") } form.Name = r.PostFormValue("Name") if len(form.Name) == 0 { return form, errors.New("名称不能为空") } form.StartAt, err = time.ParseInLocation("2006-01-02", r.PostFormValue("StartAt"), time.Local) if err != nil { return form, errors.New("开始时间格式错误") } form.EndAt, err = time.ParseInLocation("2006-01-02", r.PostFormValue("EndAt"), time.Local) if err != nil { return form, errors.New("结束时间格式错误") } if err := form.TotalMoneyF.Scan(r.PostFormValue("TotalMoney")); err != nil { return form, errors.New("总金额格式错误") } form.Description = r.PostFormValue("Description") form.ApplyAt, err = time.ParseInLocation("2006-01-02", r.PostFormValue("ApplyAt"), time.Local) if err != nil { return form, errors.New("申请时间格式错误") } form.ApplyUserID = convertor.ConvertInt[int32](r.PostFormValue("ApplyUserID"), 0) if form.ApplyUserID == 0 { return form, errors.New("申请人不能为空") } form.ManagerID = convertor.ConvertInt[int32](r.PostFormValue("ManagerID"), 0) if form.ManagerID == 0 { return form, errors.New("项目经理不能为空") } form.Members = r.PostFormValue("Members") if len(form.Members) > 0 { membersSplit := strings.SplitSeq(form.Members, ",") for v := range membersSplit { m := convertor.ConvertInt[int32](v, 0) if m == 0 { return form, errors.New("项目成员数据错误") } } } form.Status = convertor.ConvertInt[int16](r.PostFormValue("Status"), 9999) form.ProjectFiles = &formDto.ProjectFileForm{ ProjectFileItems: []*formDto.ProjectFileItemForm{}, } fns := r.PostFormValue("Paths") if len(fns) > 0 { fnsSplit := strings.SplitSeq(fns, ",") for v := range fnsSplit { if len(v) > 0 { read := strings.Split(v, "|") if len(read) != 2 { return form, errors.New("文件路径数据错误") } pff := &formDto.ProjectFileItemForm{ Name: read[0], Path: read[1], Combination: v, } form.ProjectFiles.ProjectFileItems = append(form.ProjectFiles.ProjectFileItems, pff) } } } return form, nil } func incomeExpenseEcharts(ctx context.Context, projectId int64) view.EchartsOption { var name []string var income []float64 var expense []float64 if projectId == 0 { rows, err := db.Engine.StatisticsProjects(ctx) if err != nil || len(rows) == 0 { return view.EchartsOption{} } for _, row := range rows { name = append(name, row.Name) income = append(income, convertor.NumericToFloat64(row.Income)) expense = append(expense, convertor.NumericToFloat64(row.Expense)) } } else { row, err := db.Engine.StatisticsProjectItem(ctx, projectId) if err != nil || row == nil { return view.EchartsOption{} } name = append(name, row.Name) income = append(income, convertor.NumericToFloat64(row.Income)) expense = append(expense, convertor.NumericToFloat64(row.Expense)) } return view.EchartsOption{ Title: view.Title{ Text: "项目收支情况", Left: "center", Top: 2, FontSize: 15, TextStyle: view.TextStyle{ Color: "#666666", FontWeight: "normal", }, }, Color: []string{"#fed46b", "#2194ff"}, ToolTip: view.ToolTip{ Trigger: "axis", AxisPointer: view.AxisPointer{ Type: "shadow", }, }, Grid: view.Grid{ Left: "3%", Right: "4%", Bottom: "10%", ContainLabel: true, }, Legend: view.Legend{ Left: "center", Top: "bottom", Data: []string{"支出金额", "收入金额"}, }, XAxis: []view.XAxis{ { Type: "category", Data: name, AxisTick: view.AxisTick{ AlignWithLabel: true, }, }, }, YAxis: []view.YAxis{ { Type: "value", Name: "单位:元", }, }, BarMaxWidth: "30", Label: view.Label{ Show: true, Position: "top", }, Series: []view.Series{ { Name: "支出金额", Type: "bar", Data: expense, ItemStyle: view.ItemStyle{ Normal: view.Normal{ Color: "#f89588", }, }, }, { Name: "收入金额", Type: "bar", Data: income, ItemStyle: view.ItemStyle{ Normal: view.Normal{ Color: "#76da91", }, }, }, }, } } func incomeEcharts(ctx context.Context, projectId int64) view.EchartsOption { var name []string data := []view.DataItem{} if projectId == 0 { rows, err := db.Engine.StatisticsIncome(ctx) if err != nil || len(rows) == 0 { return view.EchartsOption{} } for _, row := range rows { name = append(name, row.IncomeTypeName) data = append(data, view.DataItem{ Name: row.IncomeTypeName, Value: convertor.NumericToFloat64(row.TotalAmount), }) } } else { rows, err := db.Engine.StatisticsIncomeByProjectID(ctx, projectId) if err != nil || len(rows) == 0 { return view.EchartsOption{} } for _, row := range rows { name = append(name, row.IncomeTypeName) data = append(data, view.DataItem{ Name: row.IncomeTypeName, Value: convertor.NumericToFloat64(row.TotalAmount), }) } } return view.EchartsOption{ Title: view.Title{ Text: "收入分布", Left: "center", Orient: "vertical", FontSize: 15, TextStyle: view.TextStyle{ Color: "#666666", FontWeight: "normal", }, }, ToolTip: view.ToolTip{ Trigger: "item", }, Legend: view.Legend{ Left: "center", Top: "bottom", Data: name, }, Color: []string{"#63b2ee", "#76da91", "#f8cb7f", "#f89588", "#7cd6cf", "#9192ab", "#7898e1", "#efa666", "#eddd86", "#9987ce", "#63b2ee", "#76da91"}, Series: []view.Series{ { Type: "pie", Radius: "50%", Data: data, Label: view.Label{ Show: true, TextStyle: view.TextStyle{ Color: "#666666", }, Normal: view.LableNormal{ Formatter: "{c} ({d}%)", TextStyle: view.TextStyle{ Color: "#666666", FontWeight: "normal", }, }, }, }, }, } } func expenseEcharts(ctx context.Context, projectId int64) view.EchartsOption { var name []string data := []view.DataItem{} if projectId == 0 { rows, err := db.Engine.StatisticsExpense(ctx) if err != nil || len(rows) == 0 { return view.EchartsOption{} } for _, row := range rows { name = append(name, row.ExpensesTypeName) data = append(data, view.DataItem{ Name: row.ExpensesTypeName, Value: convertor.NumericToFloat64(row.TotalAmount), }) } } else { rows, err := db.Engine.StatisticsExpenseByProjectID(ctx, projectId) if err != nil || len(rows) == 0 { return view.EchartsOption{} } for _, row := range rows { name = append(name, row.ExpensesTypeName) data = append(data, view.DataItem{ Name: row.ExpensesTypeName, Value: convertor.NumericToFloat64(row.TotalAmount), }) } } return view.EchartsOption{ Title: view.Title{ Text: "支出分布", Left: "center", Orient: "vertical", FontSize: 15, TextStyle: view.TextStyle{ Color: "#666666", FontWeight: "normal", }, }, ToolTip: view.ToolTip{ Trigger: "item", }, Legend: view.Legend{ Left: "center", Top: "bottom", Data: name, }, Color: []string{"#63b2ee", "#76da91", "#f8cb7f", "#f89588", "#7cd6cf", "#9192ab", "#7898e1", "#efa666", "#eddd86", "#9987ce", "#63b2ee", "#76da91"}, Series: []view.Series{ { Type: "pie", Radius: "50%", Data: data, Label: view.Label{ Show: true, TextStyle: view.TextStyle{ Color: "#666666", }, Normal: view.LableNormal{ Formatter: "{c} ({d}%)", TextStyle: view.TextStyle{ Color: "#666666", FontWeight: "normal", }, }, }, }, }, } // return view.EchartsOption{ // Title: view.Title{ // Text: "支出分布", // Left: "center", // Top: 2, // FontSize: 15, // TextStyle: view.TextStyle{ // Color: "#666666", // FontWeight: "normal", // }, // }, // Color: []string{"#fed46b", "#2194ff"}, // ToolTip: view.ToolTip{ // Trigger: "axis", // AxisPointer: view.AxisPointer{ // Type: "shadow", // }, // }, // Grid: view.Grid{ // Left: "3%", // Right: "4%", // Bottom: "10%", // ContainLabel: true, // }, // XAxis: []view.XAxis{ // { // Type: "category", // Data: name, // AxisTick: view.AxisTick{ // AlignWithLabel: true, // }, // }, // }, // YAxis: []view.YAxis{ // { // Type: "value", // Name: "单位:元", // }, // }, // BarMaxWidth: "30", // Label: view.Label{ // Show: true, // Position: "top", // }, // Series: []view.Series{ // { // Type: "bar", // Data: expense, // ItemStyle: view.ItemStyle{ // Normal: view.Normal{ // Color: "#f89588", // }, // }, // }, // }, // } }