webapp
Some checks failed
Docker Build and Publish / publish (push) Failing after 1m33s

This commit is contained in:
Eugene Howe
2026-02-17 09:47:30 -05:00
parent af09672ee3
commit b0957bfa49
102 changed files with 4213 additions and 378 deletions

View File

@@ -0,0 +1,13 @@
package utils
import "strings"
func ConvertNameFormat(name string) string {
parts := strings.Split(name, ",")
if len(parts) == 2 {
lastName := strings.TrimSpace(parts[0])
firstName := strings.TrimSpace(parts[1])
return firstName + " " + lastName
}
return name
}

43
internal/utils/error.go Normal file
View File

@@ -0,0 +1,43 @@
package utils
import (
"net/http"
"github.com/swaggest/rest"
)
// CustomApplicationError is a marker interface that tells error middleware to serialize this error as-is, using Json schema.
type CustomApplicationError interface {
rest.ErrWithHTTPStatus
CustomApplicationError()
}
type ApplicationError struct {
Message string `json:"message"`
code int
}
var (
_ CustomApplicationError = ApplicationError{}
)
func NewApplicationError(message string, code int) ApplicationError {
return ApplicationError{
Message: message,
code: code,
}
}
func (e ApplicationError) Error() string {
return e.Message
}
func (e ApplicationError) HTTPStatus() int {
return e.code
}
func (e ApplicationError) CustomApplicationError() {}
func ValidationError(message string) ApplicationError {
return NewApplicationError(message, http.StatusUnprocessableEntity)
}

View File

@@ -0,0 +1,20 @@
package utils
import "time"
func FirstDayOfMonth() string {
// Load America/Detroit timezone
loc, err := time.LoadLocation("America/Detroit")
if err != nil {
panic(err)
}
// Get current time in Detroit timezone
now := time.Now().In(loc)
// Create midnight of the first day of the current month
firstDay := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
// Return ISO8601 formatted string
return firstDay.Format(time.RFC3339)
}

View File

@@ -0,0 +1,34 @@
package utils
import "strings"
func LevenshteinDistance(s1, s2 string) int {
s1 = strings.ToLower(s1)
s2 = strings.ToLower(s2)
len1 := len(s1)
len2 := len(s2)
matrix := make([][]int, len1+1)
for i := range matrix {
matrix[i] = make([]int, len2+1)
matrix[i][0] = i
}
for j := range matrix[0] {
matrix[0][j] = j
}
for i := 1; i <= len1; i++ {
for j := 1; j <= len2; j++ {
cost := 0
if s1[i-1] != s2[j-1] {
cost = 1
}
matrix[i][j] = min(
matrix[i-1][j]+1,
min(matrix[i][j-1]+1, matrix[i-1][j-1]+cost),
)
}
}
return matrix[len1][len2]
}

19
internal/utils/logging.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func LogMessageFromDatabaseOperation(entityName string, operation string, dbOp func() error) (string, error) {
err := dbOp()
logMsg := fmt.Sprintf("%s %sd successfully", cases.Title(language.English).String(entityName), operation)
if err != nil {
logMsg = fmt.Sprintf("%s %s encountered an error", cases.Title(language.English).String(operation), entityName)
}
return logMsg, err
}

12
internal/utils/must.go Normal file
View File

@@ -0,0 +1,12 @@
package utils
import "go.uber.org/zap"
func Must[T any](res T, err error) func(logger *zap.Logger) T {
return func(logger *zap.Logger) T {
if err != nil {
logger.Fatal("Fatal error", zap.Error(err))
}
return res
}
}

View File

@@ -0,0 +1,23 @@
package utils
import (
"regexp"
"strings"
"unicode"
)
func NormalizeName(name string) string {
// Remove extra whitespace
name = strings.TrimSpace(name)
name = regexp.MustCompile(`\s+`).ReplaceAllString(name, " ")
// Remove common punctuation but keep hyphens
name = strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsSpace(r) || r == '-' {
return unicode.ToLower(r)
}
return -1
}, name)
return name
}

View File

@@ -0,0 +1,105 @@
package utils
import (
"fmt"
"io"
"mime/multipart"
"regexp"
"strconv"
"strings"
"github.com/samber/lo"
)
type Shift struct {
EarningCode string `json:"earning_code"`
Description string `json:"description"`
Hours float64 `json:"hours"`
}
func (s Shift) CountsAsWorked() bool {
return strings.ToLower(s.EarningCode) == "reg"
}
type Employee struct {
Name string `json:"name"`
Shifts []Shift `json:"shifts"`
}
func (e Employee) Worked() bool {
return lo.ContainsBy(e.Shifts, func(s Shift) bool { return s.CountsAsWorked() })
}
func ParseUploadedXLSFile(file *multipart.FileHeader) ([]Employee, error) {
f, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to open XLS file: %w", err)
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read XLS file: %w", err)
}
return ParseXLSContent(content)
}
// ParseXLSContent parses the HTML content of an XLS file and returns a slice
// of employees with their shifts. The XLS files are HTML tables where:
// - Column A: employee name (td with class="smallbold" and valign=top)
// - Column B: inner table containing shift rows
// - Within the inner table, rows with colspan are category headers (skipped)
// - Shift rows have: spacer td, earning code/description td (class="smalltext"),
// and hours td
func ParseXLSContent(content []byte) ([]Employee, error) {
html := string(content)
// Split by employee rows in the outer table. Each employee row contains
// a name cell (column A) followed by a cell with an inner table (column B).
employeePattern := regexp.MustCompile(
`<td\s+valign=top\s+class="smallbold">([^<]+)</td>\s*<td>(.*?)</table></td>`,
)
employeeMatches := employeePattern.FindAllStringSubmatch(html, -1)
// Pattern for shift data rows: spacer td + earning code td + hours td
// These are rows where column C has class="smalltext" (not a colspan header)
shiftPattern := regexp.MustCompile(
`<tr><td width=25></td><td class=smalltext>([^<]+)</td><td[^>]*>([^<]+)</td></tr>`,
)
employees := lo.Map(employeeMatches, func(empMatch []string, _ int) Employee {
name := strings.TrimSpace(empMatch[1])
innerTable := empMatch[2]
shiftMatches := shiftPattern.FindAllStringSubmatch(innerTable, -1)
shifts := lo.FilterMap(shiftMatches, func(sm []string, _ int) (Shift, bool) {
codeAndDesc := strings.TrimSpace(sm[1])
hoursStr := strings.TrimSpace(sm[2])
parts := strings.SplitN(codeAndDesc, " - ", 2)
if len(parts) != 2 {
return Shift{}, false
}
hours, err := strconv.ParseFloat(hoursStr, 64)
if err != nil {
return Shift{}, false
}
return Shift{
EarningCode: strings.TrimSpace(parts[0]),
Description: strings.TrimSpace(parts[1]),
Hours: hours,
}, true
})
return Employee{
Name: name,
Shifts: shifts,
}
})
return employees, nil
}

View File

@@ -0,0 +1,77 @@
package utils_test
import (
"os"
"clintonambulance.com/calculate_negative_points/internal/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("ParseXLSContent", func() {
It("parses employees and their shifts from the test XLS file", func() {
content, err := os.ReadFile("testdata/test.xls")
Expect(err).NotTo(HaveOccurred())
employees, err := utils.ParseXLSContent(content)
Expect(err).NotTo(HaveOccurred())
Expect(employees).To(HaveLen(2))
By("parsing the first employee")
Expect(employees[0].Name).To(Equal("Howe, Eugene"))
Expect(employees[0].Worked()).To(BeTrue())
Expect(employees[0].Shifts).To(HaveLen(2))
Expect(employees[0].Shifts[0]).To(Equal(utils.Shift{
EarningCode: "MEET",
Description: "Meetings",
Hours: 1.75,
}))
Expect(employees[0].Shifts[0].CountsAsWorked()).To(BeFalse())
Expect(employees[0].Shifts[1]).To(Equal(utils.Shift{
EarningCode: "Reg",
Description: "Regular Hours",
Hours: 125.75,
}))
Expect(employees[0].Shifts[1].CountsAsWorked()).To(BeTrue())
By("parsing the second employee with multiple shift categories")
Expect(employees[1].Name).To(Equal("User, Test"))
Expect(employees[1].Shifts).To(HaveLen(4))
Expect(employees[1].Shifts[0]).To(Equal(utils.Shift{
EarningCode: "MEET",
Description: "Meetings",
Hours: 2.00,
}))
Expect(employees[1].Shifts[1]).To(Equal(utils.Shift{
EarningCode: "Reg",
Description: "Regular Hours",
Hours: 134.00,
}))
Expect(employees[1].Shifts[2]).To(Equal(utils.Shift{
EarningCode: "Holiday",
Description: "Holiday Hours",
Hours: 8.00,
}))
Expect(employees[1].Shifts[3]).To(Equal(utils.Shift{
EarningCode: "PTO",
Description: "Paid Time Off",
Hours: 12.00,
}))
})
It("skips category header rows (colspan rows)", func() {
content, err := os.ReadFile("testdata/test.xls")
Expect(err).NotTo(HaveOccurred())
employees, err := utils.ParseXLSContent(content)
Expect(err).NotTo(HaveOccurred())
Expect(employees).To(HaveLen(2))
})
It("returns an empty slice for empty content", func() {
employees, err := utils.ParseXLSContent([]byte(""))
Expect(err).NotTo(HaveOccurred())
Expect(employees).To(BeEmpty())
})
})

View File

@@ -0,0 +1,14 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
func RandomString(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

View File

@@ -0,0 +1,11 @@
package schemautils
type OpenfeatureFlag struct {
Name string `json:"name" required:"true" description:"Name of the flag"`
Id string `json:"id" required:"true" description:"UUID of the flag"`
Kind string `json:"kind" required:"true" description:"Flag kind" enum:"boolean,number,string,string_array,int_array,float_array"`
Enabled bool `json:"enabled" required:"true" description:"This is hardcoded to true"`
Variant string `json:"variant" required:"true" description:"The name of the variant that is assigned to the appclient environment"`
Variants map[string]interface{} `json:"variants" required:"true" description:"All variants and their values" nullable:"false"`
Default string `json:"default" required:"true" description:"Name of the default variant assigned to the flag"`
}

View File

@@ -0,0 +1,24 @@
package utils
import "strings"
func ToTitleCase(name string) string {
words := strings.Fields(name)
for i, word := range words {
if len(word) > 0 {
// Handle hyphenated names
if strings.Contains(word, "-") {
parts := strings.Split(word, "-")
for j, part := range parts {
if len(part) > 0 {
parts[j] = strings.ToUpper(string(part[0])) + strings.ToLower(part[1:])
}
}
words[i] = strings.Join(parts, "-")
} else {
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
}
}
}
return strings.Join(words, " ")
}

View File

@@ -0,0 +1,13 @@
package utils_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestUtils(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Utils Suite")
}