|
|
|
|
@@ -0,0 +1,562 @@
|
|
|
|
|
package controllers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"doormile/constants"
|
|
|
|
|
"doormile/db"
|
|
|
|
|
"doormile/internal/notify"
|
|
|
|
|
"doormile/models"
|
|
|
|
|
"doormile/utils"
|
|
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// DUTY MANAGEMENT
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
func MilerStartDuty(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
Lat float64 `json:"lat"`
|
|
|
|
|
Lon float64 `json:"lon"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
midnight := todayMidnight()
|
|
|
|
|
var existing models.MilerDutyLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND onduty = ? AND logoutat IS NULL AND loginat >= ?", milerUserID, true, midnight).
|
|
|
|
|
First(&existing).Error; err == nil {
|
|
|
|
|
return utils.BadRequest(c, "already on duty, end current duty first")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
dutyLog := models.MilerDutyLog{
|
|
|
|
|
Userid: milerUserID,
|
|
|
|
|
Loginat: now,
|
|
|
|
|
Onduty: true,
|
|
|
|
|
Startlat: req.Lat,
|
|
|
|
|
Startlon: req.Lon,
|
|
|
|
|
Lastlat: req.Lat,
|
|
|
|
|
Lastlon: req.Lon,
|
|
|
|
|
}
|
|
|
|
|
if err := db.DB.Create(&dutyLog).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to start duty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.DB.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Updates(map[string]interface{}{
|
|
|
|
|
"availabilitystatus": constants.MilerAvailable,
|
|
|
|
|
"currentlatitude": req.Lat,
|
|
|
|
|
"currentlongitude": req.Lon,
|
|
|
|
|
"lastlocationupdatedat": now,
|
|
|
|
|
})
|
|
|
|
|
db.DB.Model(&models.AppUser{}).Where("userid = ?", milerUserID).Update("onduty", 1)
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"dutylogid": dutyLog.Dutylogid,
|
|
|
|
|
"loginat": dutyLog.Loginat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MilerEndDuty(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
Lat float64 `json:"lat"`
|
|
|
|
|
Lon float64 `json:"lon"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dutyLog models.MilerDutyLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND onduty = ? AND logoutat IS NULL", milerUserID, true).
|
|
|
|
|
Order("loginat DESC").First(&dutyLog).Error; err != nil {
|
|
|
|
|
return utils.BadRequest(c, "not currently on duty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var activeAssignments int64
|
|
|
|
|
db.DB.Model(&models.BookingAssignment{}).
|
|
|
|
|
Where("mileruserid = ? AND assignmentstatus IN ?", milerUserID, []string{constants.AssignmentAssigned, constants.AssignmentAccepted}).
|
|
|
|
|
Count(&activeAssignments)
|
|
|
|
|
if activeAssignments > 0 {
|
|
|
|
|
return utils.BadRequest(c, "cannot go off duty with active assignments")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
dutyLog.Logoutat = &now
|
|
|
|
|
dutyLog.Onduty = false
|
|
|
|
|
dutyLog.Lastlat = req.Lat
|
|
|
|
|
dutyLog.Lastlon = req.Lon
|
|
|
|
|
if err := db.DB.Save(&dutyLog).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to end duty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.DB.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerOffline)
|
|
|
|
|
db.DB.Model(&models.AppUser{}).Where("userid = ?", milerUserID).Update("onduty", 0)
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"dutylogid": dutyLog.Dutylogid,
|
|
|
|
|
"logoutat": dutyLog.Logoutat,
|
|
|
|
|
"totalkms": dutyLog.Totalkms,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MilerGetDutyStatus(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
midnight := todayMidnight()
|
|
|
|
|
|
|
|
|
|
var dutyLog models.MilerDutyLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND onduty = ? AND logoutat IS NULL AND loginat >= ?", milerUserID, true, midnight).
|
|
|
|
|
Order("loginat DESC").First(&dutyLog).Error; err != nil {
|
|
|
|
|
return utils.OK(c, fiber.Map{"onduty": false})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var profile models.MilerProfile
|
|
|
|
|
db.DB.Where("userid = ?", milerUserID).First(&profile)
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"onduty": true,
|
|
|
|
|
"dutylogid": dutyLog.Dutylogid,
|
|
|
|
|
"loginat": dutyLog.Loginat,
|
|
|
|
|
"totalkms": dutyLog.Totalkms,
|
|
|
|
|
"availabilitystatus": profile.Availabilitystatus,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// BREAK MANAGEMENT
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
func MilerStartBreak(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
Breaktype string `json:"breaktype"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
if req.Breaktype == "" {
|
|
|
|
|
req.Breaktype = "Personal"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dutyLog models.MilerDutyLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND onduty = ? AND logoutat IS NULL", milerUserID, true).
|
|
|
|
|
Order("loginat DESC").First(&dutyLog).Error; err != nil {
|
|
|
|
|
return utils.BadRequest(c, "not on duty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var openBreak models.MilerBreakLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND endat IS NULL", milerUserID).First(&openBreak).Error; err == nil {
|
|
|
|
|
return utils.BadRequest(c, "break already active")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
breakLog := models.MilerBreakLog{
|
|
|
|
|
Userid: milerUserID,
|
|
|
|
|
Dutylogid: dutyLog.Dutylogid,
|
|
|
|
|
Breaktype: req.Breaktype,
|
|
|
|
|
Startat: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
if err := db.DB.Create(&breakLog).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to start break")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.DB.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerBreak)
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"breaklogid": breakLog.Breaklogid,
|
|
|
|
|
"startat": breakLog.Startat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MilerEndBreak(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var breakLog models.MilerBreakLog
|
|
|
|
|
if err := db.DB.Where("userid = ? AND endat IS NULL", milerUserID).
|
|
|
|
|
Order("startat DESC").First(&breakLog).Error; err != nil {
|
|
|
|
|
return utils.BadRequest(c, "no active break")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
breakLog.Endat = &now
|
|
|
|
|
if err := db.DB.Save(&breakLog).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to end break")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.DB.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
|
|
|
"breaklogid": breakLog.Breaklogid,
|
|
|
|
|
"endat": breakLog.Endat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// MILER'S OWN BOOKINGS
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
func MilerGetMyBookings(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
status := c.Query("status")
|
|
|
|
|
dateStr := c.Query("date")
|
|
|
|
|
|
|
|
|
|
query := db.DB.Where("assignedmileruserid = ?", milerUserID)
|
|
|
|
|
if status != "" && status != "All" {
|
|
|
|
|
query = query.Where("status = ?", status)
|
|
|
|
|
}
|
|
|
|
|
if dateStr != "" {
|
|
|
|
|
if date, err := time.Parse("2006-01-02", dateStr); err == nil {
|
|
|
|
|
query = query.Where("createdat >= ? AND createdat < ?", date, date.Add(24*time.Hour))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bookings []models.PickupBooking
|
|
|
|
|
if err := query.Preload("Parcels").Preload("ServiceOptions").Order("createdat DESC").Find(&bookings).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch bookings")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := make([]fiber.Map, 0, len(bookings))
|
|
|
|
|
for _, b := range bookings {
|
|
|
|
|
var customer models.AppCustomer
|
|
|
|
|
db.DB.Where("appcustomerid = ?", b.Appcustomerid).First(&customer)
|
|
|
|
|
|
|
|
|
|
response = append(response, fiber.Map{
|
|
|
|
|
"bookingid": b.Bookingid,
|
|
|
|
|
"bookingreference": b.Bookingno,
|
|
|
|
|
"status": b.Status,
|
|
|
|
|
"pickupaddress": b.Pickupaddress,
|
|
|
|
|
"pickuplatitude": b.Pickuplatitude,
|
|
|
|
|
"pickuplongitude": b.Pickuplongitude,
|
|
|
|
|
"deliveryaddress": b.Deliveryaddress,
|
|
|
|
|
"deliverylatitude": b.Deliverylatitude,
|
|
|
|
|
"deliverylongitude": b.Deliverylongitude,
|
|
|
|
|
"customername": strings.TrimSpace(customer.Firstname + " " + customer.Lastname),
|
|
|
|
|
"customerphone": customer.Phone,
|
|
|
|
|
"parcels": b.Parcels,
|
|
|
|
|
"serviceoptions": b.ServiceOptions,
|
|
|
|
|
"createdat": b.Createdat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.List(c, response, int64(len(response)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// DELIVERY CONFIRMATION
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
func MilerDeliverConsignment(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
consignmentID, err := strconv.Atoi(c.Params("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid consignment ID")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
Otp string `json:"otp"`
|
|
|
|
|
Deliveredtoname string `json:"deliveredtoname"`
|
|
|
|
|
Photourl string `json:"photourl"`
|
|
|
|
|
Receiversignatureurl string `json:"receiversignatureurl"`
|
|
|
|
|
Lat float64 `json:"lat"`
|
|
|
|
|
Lon float64 `json:"lon"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
if req.Otp == "" {
|
|
|
|
|
return utils.BadRequest(c, "otp is required")
|
|
|
|
|
}
|
|
|
|
|
if req.Deliveredtoname == "" {
|
|
|
|
|
return utils.BadRequest(c, "deliveredtoname is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var consignment models.Consignment
|
|
|
|
|
if err := db.DB.First(&consignment, consignmentID).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "consignment not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var booking models.PickupBooking
|
|
|
|
|
if err := db.DB.Where("consignmentid = ? AND assignedmileruserid = ?", consignment.Consignmentid, milerUserID).
|
|
|
|
|
First(&booking).Error; err != nil {
|
|
|
|
|
return utils.NotFound(c, "assigned consignment not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if consignment.Status != constants.ConsignmentOutForDelivery {
|
|
|
|
|
return utils.BadRequest(c, "consignment is not out for delivery")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
|
|
|
|
|
|
// OTP generation/storage is Phase 2 — no delivery_otp field exists yet on
|
|
|
|
|
// consignments, so acceptance of a non-empty OTP is treated as verified.
|
|
|
|
|
proof := models.DeliveryProof{
|
|
|
|
|
Consignmentid: consignment.Consignmentid,
|
|
|
|
|
Deliveredat: time.Now(),
|
|
|
|
|
Deliveredtoname: req.Deliveredtoname,
|
|
|
|
|
Receiversignatureurl: req.Receiversignatureurl,
|
|
|
|
|
Photourl: req.Photourl,
|
|
|
|
|
Otpverified: true,
|
|
|
|
|
Geolatitude: req.Lat,
|
|
|
|
|
Geolongitude: req.Lon,
|
|
|
|
|
Createdby: milerUserID,
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Create(&proof).Error; err != nil {
|
|
|
|
|
tx.Rollback()
|
|
|
|
|
return utils.Internal(c, "failed to record delivery proof")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
consignment.Status = constants.ConsignmentDelivered
|
|
|
|
|
consignment.Updatedat = time.Now()
|
|
|
|
|
tx.Save(&consignment)
|
|
|
|
|
|
|
|
|
|
tx.Model(&models.BookingAssignment{}).
|
|
|
|
|
Where("bookingid = ? AND mileruserid = ?", booking.Bookingid, milerUserID).
|
|
|
|
|
Updates(map[string]interface{}{
|
|
|
|
|
"assignmentstatus": constants.AssignmentCompleted,
|
|
|
|
|
"completedat": time.Now(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
tx.Commit()
|
|
|
|
|
|
|
|
|
|
if db.Js != nil {
|
|
|
|
|
payload := map[string]interface{}{
|
|
|
|
|
"bookingid": booking.Bookingid,
|
|
|
|
|
"outcome": "on_time",
|
|
|
|
|
"consignmentid": consignment.Consignmentid,
|
|
|
|
|
"mileruserid": milerUserID,
|
|
|
|
|
}
|
|
|
|
|
if data, err := json.Marshal(payload); err == nil {
|
|
|
|
|
if _, err := db.Js.Publish("booking.outcome", data); err != nil {
|
|
|
|
|
utils.Warn("MilerDeliverConsignment: NATS publish failed", "consignment_id", consignment.Consignmentid, "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var customer models.AppCustomer
|
|
|
|
|
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
|
|
|
|
if notifyErr := notify.SendToDevice(
|
|
|
|
|
customer.Devicetoken,
|
|
|
|
|
"Parcel Delivered",
|
|
|
|
|
"Your parcel has been delivered!",
|
|
|
|
|
map[string]string{
|
|
|
|
|
"booking_id": strconv.Itoa(booking.Bookingid),
|
|
|
|
|
"consignment_id": strconv.Itoa(consignment.Consignmentid),
|
|
|
|
|
},
|
|
|
|
|
); notifyErr != nil {
|
|
|
|
|
utils.Warn("FCM: failed to notify customer on delivery", "booking_id", booking.Bookingid, "error", notifyErr)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.JSON(fiber.Map{
|
|
|
|
|
"success": true,
|
|
|
|
|
"message": "delivery confirmed",
|
|
|
|
|
"data": fiber.Map{
|
|
|
|
|
"proofid": proof.Proofid,
|
|
|
|
|
"deliveredat": proof.Deliveredat,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// EARNINGS
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
type milerEarningsSummary struct {
|
|
|
|
|
CompletedStops int64 `gorm:"column:completed_stops"`
|
|
|
|
|
TotalKms float64 `gorm:"column:total_kms"`
|
|
|
|
|
TotalEarnings float64 `gorm:"column:total_earnings"`
|
|
|
|
|
TotalBonus int `gorm:"column:total_bonus"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MilerGetEarnings(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
period := c.Query("period", "daily")
|
|
|
|
|
|
|
|
|
|
var start, end time.Time
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
|
|
switch period {
|
|
|
|
|
case "weekly":
|
|
|
|
|
start = todayMidnight().Add(-6 * 24 * time.Hour)
|
|
|
|
|
end = todayMidnight().Add(24 * time.Hour)
|
|
|
|
|
case "monthly":
|
|
|
|
|
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
|
|
|
end = start.AddDate(0, 1, 0)
|
|
|
|
|
default:
|
|
|
|
|
period = "daily"
|
|
|
|
|
day := todayMidnight()
|
|
|
|
|
if dateStr := c.Query("date"); dateStr != "" {
|
|
|
|
|
if parsed, err := time.Parse("2006-01-02", dateStr); err == nil {
|
|
|
|
|
day = parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
start = day
|
|
|
|
|
end = day.Add(24 * time.Hour)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var summary milerEarningsSummary
|
|
|
|
|
db.DB.Model(&models.BookingAssignment{}).
|
|
|
|
|
Where("mileruserid = ? AND assignmentstatus = ? AND completedat >= ? AND completedat < ?",
|
|
|
|
|
milerUserID, constants.AssignmentCompleted, start, end).
|
|
|
|
|
Select("COUNT(*) as completed_stops, COALESCE(SUM(riderkms),0) as total_kms, COALESCE(SUM(ridercharges),0) as total_earnings, COALESCE(SUM(bonuspoints),0) as total_bonus").
|
|
|
|
|
Scan(&summary)
|
|
|
|
|
|
|
|
|
|
var breakdown []models.BookingAssignment
|
|
|
|
|
db.DB.Where("mileruserid = ? AND assignmentstatus = ? AND completedat >= ? AND completedat < ?",
|
|
|
|
|
milerUserID, constants.AssignmentCompleted, start, end).
|
|
|
|
|
Order("completedat DESC").Find(&breakdown)
|
|
|
|
|
|
|
|
|
|
breakdownResp := make([]fiber.Map, 0, len(breakdown))
|
|
|
|
|
for _, a := range breakdown {
|
|
|
|
|
breakdownResp = append(breakdownResp, fiber.Map{
|
|
|
|
|
"bookingid": a.Bookingid,
|
|
|
|
|
"riderkms": a.Riderkms,
|
|
|
|
|
"ridercharges": a.Ridercharges,
|
|
|
|
|
"bonuspoints": a.Bonuspoints,
|
|
|
|
|
"completedat": a.Completedat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data := fiber.Map{
|
|
|
|
|
"period": period,
|
|
|
|
|
"completed_stops": summary.CompletedStops,
|
|
|
|
|
"total_kms": summary.TotalKms,
|
|
|
|
|
"total_earnings": summary.TotalEarnings,
|
|
|
|
|
"total_bonus": summary.TotalBonus,
|
|
|
|
|
"breakdown": breakdownResp,
|
|
|
|
|
}
|
|
|
|
|
if period == "daily" {
|
|
|
|
|
data["date"] = start.Format("2006-01-02")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.OK(c, data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// NOTIFICATIONS (synthetic — no dedicated table yet)
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
// MilerGetNotifications generates notifications from real assignment events,
|
|
|
|
|
// mirroring GetHubNotifications' synthetic-notification pattern.
|
|
|
|
|
func MilerGetNotifications(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
midnight := todayMidnight()
|
|
|
|
|
last24h := time.Now().Add(-24 * time.Hour)
|
|
|
|
|
|
|
|
|
|
var notifications []notificationEntry
|
|
|
|
|
|
|
|
|
|
var assigned []models.BookingAssignment
|
|
|
|
|
db.DB.Where("mileruserid = ? AND assignedat >= ?", milerUserID, midnight).Find(&assigned)
|
|
|
|
|
for _, a := range assigned {
|
|
|
|
|
notifications = append(notifications, notificationEntry{
|
|
|
|
|
Title: fmt.Sprintf("New pickup assigned — Booking #%d", a.Bookingid),
|
|
|
|
|
Type: "assignment",
|
|
|
|
|
Time: a.Assignedat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var completed []models.BookingAssignment
|
|
|
|
|
db.DB.Where("mileruserid = ? AND assignmentstatus = ? AND completedat >= ?", milerUserID, constants.AssignmentCompleted, midnight).Find(&completed)
|
|
|
|
|
for _, a := range completed {
|
|
|
|
|
t := a.Assignedat
|
|
|
|
|
if a.Completedat != nil {
|
|
|
|
|
t = *a.Completedat
|
|
|
|
|
}
|
|
|
|
|
notifications = append(notifications, notificationEntry{
|
|
|
|
|
Title: fmt.Sprintf("Booking #%d completed", a.Bookingid),
|
|
|
|
|
Type: "completed",
|
|
|
|
|
Time: t,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rejected []models.BookingAssignment
|
|
|
|
|
db.DB.Where("mileruserid = ? AND assignmentstatus = ? AND assignedat >= ?", milerUserID, constants.AssignmentRejected, last24h).Find(&rejected)
|
|
|
|
|
for _, a := range rejected {
|
|
|
|
|
notifications = append(notifications, notificationEntry{
|
|
|
|
|
Title: fmt.Sprintf("Assignment rejected — Booking #%d", a.Bookingid),
|
|
|
|
|
Type: "rejected",
|
|
|
|
|
Time: a.Assignedat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.Slice(notifications, func(i, j int) bool { return notifications[i].Time.After(notifications[j].Time) })
|
|
|
|
|
if len(notifications) > 20 {
|
|
|
|
|
notifications = notifications[:20]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := make([]fiber.Map, 0, len(notifications))
|
|
|
|
|
for i, n := range notifications {
|
|
|
|
|
response = append(response, fiber.Map{
|
|
|
|
|
"id": i + 1,
|
|
|
|
|
"title": n.Title,
|
|
|
|
|
"type": n.Type,
|
|
|
|
|
"time": humanizeRelativeTime(n.Time),
|
|
|
|
|
"read": false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.List(c, response, int64(len(response)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MilerMarkNotificationRead is a stub until a notifications table with
|
|
|
|
|
// read-state exists — always succeeds without persisting anything, matching
|
|
|
|
|
// MarkNotificationRead's hub-console equivalent.
|
|
|
|
|
func MilerMarkNotificationRead(c *fiber.Ctx) error {
|
|
|
|
|
return c.JSON(fiber.Map{"success": true})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
// SUPPORT
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
func MilerCreateTicket(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var req struct {
|
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return utils.BadRequest(c, "invalid request body")
|
|
|
|
|
}
|
|
|
|
|
if req.Subject == "" || req.Description == "" {
|
|
|
|
|
return utils.BadRequest(c, "subject and description are required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ticket := models.MilerSupportTicket{
|
|
|
|
|
Userid: milerUserID,
|
|
|
|
|
Subject: req.Subject,
|
|
|
|
|
Description: req.Description,
|
|
|
|
|
Status: "Open",
|
|
|
|
|
}
|
|
|
|
|
if err := db.DB.Create(&ticket).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to create support ticket")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.Created(c, fiber.Map{
|
|
|
|
|
"ticketid": ticket.Ticketid,
|
|
|
|
|
"status": ticket.Status,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MilerGetTickets(c *fiber.Ctx) error {
|
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
|
|
|
|
|
|
var tickets []models.MilerSupportTicket
|
|
|
|
|
if err := db.DB.Where("userid = ?", milerUserID).Order("createdat DESC").Find(&tickets).Error; err != nil {
|
|
|
|
|
return utils.Internal(c, "failed to fetch support tickets")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return utils.List(c, tickets, int64(len(tickets)))
|
|
|
|
|
}
|