diff --git a/server/controllers/import.go b/server/controllers/import.go index 38a897d..3ea9a9d 100644 --- a/server/controllers/import.go +++ b/server/controllers/import.go @@ -9,6 +9,7 @@ import ( func RegisteImportController(router *gin.RouterGroup) { router.POST("/import/fuelly", fuellyImport) + router.POST("/import/drivvo", drivvoImport) } func fuellyImport(c *gin.Context) { @@ -24,3 +25,17 @@ func fuellyImport(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{}) } + +func drivvoImport(c *gin.Context) { + bytes, err := getFileBytes(c, "file") + if err != nil { + c.JSON(http.StatusUnprocessableEntity, err) + return + } + errors := service.DrivvoImport(bytes, c.MustGet("userId").(string)) + if len(errors) > 0 { + c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors}) + return + } + c.JSON(http.StatusOK, gin.H{}) +} diff --git a/server/service/importService.go b/server/service/importService.go index dbd5eab..2c97d09 100644 --- a/server/service/importService.go +++ b/server/service/importService.go @@ -11,6 +11,188 @@ import ( "github.com/leekchan/accounting" ) +// TODO: Move drivvo stuff to separate file + +func DrivvoParseExpenses(content []byte, user *db.User) ([]db.Expense, []string) { + expenseReader := csv.NewReader(bytes.NewReader(content)) + expenseReader.Comment = '#' + // Read headers (there is a trailing comma at the end, that's why we have to read the first line) + expenseReader.Read() + expenseReader.FieldsPerRecord = 6 + expenseRecords, err := expenseReader.ReadAll() + + var errors []string + if err != nil { + errors = append(errors, err.Error()) + println(err.Error()) + return nil, errors + } + + var expenses []db.Expense + for index, record := range expenseRecords { + expense := db.Expense{} + + date, err := time.Parse("2006-01-02 15:04:05", record[1]) + if err != nil { + errors = append(errors, "Found an invalid date/time at service/expense row "+strconv.Itoa(index+1)) + } + expense.Date = date + + totalCost, err := strconv.ParseFloat(record[2], 32) + if err != nil { + errors = append(errors, "Found and invalid total cost at service/expense row "+strconv.Itoa(index+1)) + } + expense.Amount = float32(totalCost) + + odometer, err := strconv.Atoi(record[0]) + if err != nil { + errors = append(errors, "Found an invalid odometer reading at service/expense row "+strconv.Itoa(index+1)) + } + expense.OdoReading = odometer + + notes := fmt.Sprintf("Location: %s\nNotes: %s\n", record[4], record[5]) + expense.Comments = notes + + expense.ExpenseType = record[3] + expense.UserID = user.ID + expense.Currency = user.Currency + expense.DistanceUnit = user.DistanceUnit + expense.Source = "Drivvo" + + expenses = append(expenses, expense) + } + + return expenses, errors +} + +func DrivvoParseRefuelings(content []byte, user *db.User) ([]db.Fillup, []string) { + refuelingReader := csv.NewReader(bytes.NewReader(content)) + refuelingReader.Comment = '#' + refuelingRecords, err := refuelingReader.ReadAll() + + var errors []string + if err != nil { + errors = append(errors, err.Error()) + println(err.Error()) + return nil, errors + } + + var fillups []db.Fillup + for index, record := range refuelingRecords { + // Skip column titles + if index == 0 { + continue + } + + fillup := db.Fillup{} + + date, err := time.Parse("2006-01-02 15:04:05", record[1]) + if err != nil { + errors = append(errors, "Found an invalid date/time at refuel row "+strconv.Itoa(index+1)) + } + fillup.Date = date + + totalCost, err := strconv.ParseFloat(record[4], 32) + if err != nil { + errors = append(errors, "Found and invalid total cost at refuel row "+strconv.Itoa(index+1)) + } + fillup.TotalAmount = float32(totalCost) + + odometer, err := strconv.Atoi(record[0]) + if err != nil { + errors = append(errors, "Found an invalid odometer reading at refuel row "+strconv.Itoa(index+1)) + } + fillup.OdoReading = odometer + + // TODO: Make optional + location := record[17] + fillup.FillingStation = location + + pricePerUnit, err := strconv.ParseFloat(record[3], 32) + if err != nil { + // TODO: Add unit type to error message + errors = append(errors, "Found an invalid cost per unit at refuel row "+strconv.Itoa(index+1)) + } + fillup.PerUnitPrice = float32(pricePerUnit) + + quantity, err := strconv.ParseFloat(record[5], 32) + if err != nil { + errors = append(errors, "Found an invalid quantity at refuel row "+strconv.Itoa(index+1)) + } + fillup.FuelQuantity = float32(quantity) + + isTankFull := record[6] == "Yes" + fillup.IsTankFull = &isTankFull + + // Unfortunatly, drivvo doesn't expose this info in their export + fal := false + fillup.HasMissedFillup = &fal + + notes := fmt.Sprintf("Reason: %s\nNotes: %s\nFuel: %s\n", record[18], record[19], record[2]) + fillup.Comments = notes + + fillup.UserID = user.ID + fillup.Currency = user.Currency + fillup.DistanceUnit = user.DistanceUnit + fillup.Source = "Drivvo" + + fillups = append(fillups, fillup) + } + return fillups, errors +} + +func DrivvoImport(content []byte, userId string) []string { + var errors []string + user, err := GetUserById(userId) + if err != nil { + errors = append(errors, err.Error()) + return errors + } + + serviceSectionIndex := bytes.Index(content, []byte("#Service")) + + endParseIndex := bytes.Index(content, []byte("#Income")) + if endParseIndex == -1 { + endParseIndex = bytes.Index(content, []byte("#Route")) + if endParseIndex == -1 { + endParseIndex = len(content) + } + + } + + expenseSectionIndex := bytes.Index(content, []byte("#Expense")) + if expenseSectionIndex == -1 { + expenseSectionIndex = endParseIndex + } + + fillups, errors := DrivvoParseRefuelings(content[:serviceSectionIndex], user) + _ = fillups + + var allExpenses []db.Expense + if serviceSectionIndex != -1 { + services, parseErrors := DrivvoParseExpenses(content[serviceSectionIndex:expenseSectionIndex], user) + if parseErrors != nil { + errors = append(errors, parseErrors...) + } + allExpenses = append(allExpenses, services...) + } + + if expenseSectionIndex != endParseIndex { + expenses, parseErrors := DrivvoParseExpenses(content[expenseSectionIndex:endParseIndex], user) + if parseErrors != nil { + errors = append(errors, parseErrors...) + } + allExpenses = append(allExpenses, expenses...) + } + + if len(errors) != 0 { + return errors + } + + errors = append(errors, "Not implemented") + return errors +} + func FuellyImport(content []byte, userId string) []string { stream := bytes.NewReader(content) reader := csv.NewReader(stream) diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 82495f7..011de78 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -410,6 +410,15 @@ export default [ }, props: (route) => ({ user: store.state.auth.currentUser || {} }), }, + { + path: '/import/drivvo', + name: 'import-drivvo', + component: () => lazyLoadView(import('@views/import-drivvo.vue')), + meta: { + authRequired: true, + }, + props: (route) => ({ user: store.state.auth.currentUser || {} }), + }, { path: '/logout', name: 'logout', diff --git a/ui/src/router/views/import-drivvo.vue b/ui/src/router/views/import-drivvo.vue new file mode 100644 index 0000000..f30b377 --- /dev/null +++ b/ui/src/router/views/import-drivvo.vue @@ -0,0 +1,146 @@ + + + diff --git a/ui/src/router/views/import.vue b/ui/src/router/views/import.vue index f92b9f9..0742cbd 100644 --- a/ui/src/router/views/import.vue +++ b/ui/src/router/views/import.vue @@ -26,11 +26,22 @@ export default { >
-
-

Fuelly

-

If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.

-
- Import +
+
+

Fuelly

+

If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.

+
+ Import +
+
+ +
+
+

Drivvo

+

Import data stored in Drivvo to Hammond by exporting the CSV file from Drivvo and uploading it here.

+
+ Import +