new changes in hub api messages
This commit is contained in:
@@ -218,6 +218,12 @@ func GetHubDashboardStats(c *fiber.Ctx) error {
|
||||
Where("hubid = ? AND createdat >= ?", hubID, midnight).
|
||||
Count(&exceptions)
|
||||
|
||||
// Sorting happens at inbound scan (CreateInboundScan assigns a shelf), so
|
||||
// today's inwarded count doubles as "parcels sorted today"; the target is
|
||||
// the hub's own declared daily capacity rather than a made-up constant.
|
||||
var hub models.Hub
|
||||
db.DB.Where("hubid = ?", hubID).First(&hub)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"parcels_received_today": parcelsReceivedToday,
|
||||
"milers_available": milersAvailable,
|
||||
@@ -225,6 +231,8 @@ func GetHubDashboardStats(c *fiber.Ctx) error {
|
||||
"pending_pickups": pendingPickups,
|
||||
"batches_sent_today": batchesSentToday,
|
||||
"exceptions": exceptions,
|
||||
"parcels_sorted": parcelsReceivedToday,
|
||||
"sorting_target": hub.Capacity,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -606,6 +614,14 @@ func GetHubMilers(c *fiber.Ctx) error {
|
||||
hoursActive = time.Since(*checkinAt).Hours()
|
||||
}
|
||||
|
||||
var vehicleNo string
|
||||
if mp.Vehicleid != nil {
|
||||
var v models.Vehicle
|
||||
if db.DB.Where("vehicleid = ?", *mp.Vehicleid).First(&v).Error == nil {
|
||||
vehicleNo = v.Vehicleno
|
||||
}
|
||||
}
|
||||
|
||||
response = append(response, fiber.Map{
|
||||
"milerprofileid": mp.Milerprofileid,
|
||||
"userid": mp.Userid,
|
||||
@@ -614,6 +630,7 @@ func GetHubMilers(c *fiber.Ctx) error {
|
||||
"vehicleid": mp.Vehicleid,
|
||||
"hubid": mp.Hubid,
|
||||
"defaultvehicletype": mp.Defaultvehicletype,
|
||||
"vehicleno": vehicleNo,
|
||||
"currentlatitude": mp.Currentlatitude,
|
||||
"currentlongitude": mp.Currentlongitude,
|
||||
"currentpincode": mp.Currentpincode,
|
||||
@@ -636,6 +653,174 @@ func GetHubMilers(c *fiber.Ctx) error {
|
||||
return utils.List(c, response, int64(len(response)))
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// HUB MESSAGES / CHAT
|
||||
// --------------------
|
||||
|
||||
// ensureHubConversations lazily creates a HubConversation for every miler
|
||||
// currently assigned to this hub that doesn't already have one, so the
|
||||
// message list always reflects the hub's real roster without needing a
|
||||
// separate provisioning endpoint.
|
||||
func ensureHubConversations(hubID int) {
|
||||
var profiles []models.MilerProfile
|
||||
if err := db.DB.Where("hubid = ?", hubID).Find(&profiles).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, mp := range profiles {
|
||||
var existing models.HubConversation
|
||||
if db.DB.Where("hubid = ? AND mileruserid = ?", hubID, mp.Userid).First(&existing).Error == nil {
|
||||
continue
|
||||
}
|
||||
userid := mp.Userid
|
||||
conv := models.HubConversation{
|
||||
Hubid: hubID,
|
||||
Mileruserid: &userid,
|
||||
Participantname: mp.Displayname,
|
||||
Participantrole: "Miler",
|
||||
}
|
||||
db.DB.Create(&conv)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHubMessages returns the hub's conversation list, ordered by most recent
|
||||
// activity, with each conversation's last message and unread count.
|
||||
func GetHubMessages(c *fiber.Ctx) error {
|
||||
hubID := c.Locals("hubid").(int)
|
||||
ensureHubConversations(hubID)
|
||||
|
||||
var conversations []models.HubConversation
|
||||
if err := db.DB.Where("hubid = ?", hubID).Order("updatedat DESC").Find(&conversations).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch conversations")
|
||||
}
|
||||
|
||||
response := make([]fiber.Map, 0, len(conversations))
|
||||
for _, conv := range conversations {
|
||||
var lastMsg models.HubMessage
|
||||
lastMessageText := ""
|
||||
lastTime := conv.Createdat
|
||||
if db.DB.Where("hubconversationid = ?", conv.Hubconversationid).
|
||||
Order("createdat DESC").First(&lastMsg).Error == nil {
|
||||
lastMessageText = lastMsg.Messagetext
|
||||
lastTime = lastMsg.Createdat
|
||||
}
|
||||
|
||||
var unread int64
|
||||
db.DB.Model(&models.HubMessage{}).
|
||||
Where("hubconversationid = ? AND sender = 'them' AND isread = false", conv.Hubconversationid).
|
||||
Count(&unread)
|
||||
|
||||
response = append(response, fiber.Map{
|
||||
"id": conv.Hubconversationid,
|
||||
"name": conv.Participantname,
|
||||
"lastmessage": lastMessageText,
|
||||
"time": humanizeRelativeTime(lastTime),
|
||||
"unread": unread,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.List(c, response, int64(len(response)))
|
||||
}
|
||||
|
||||
// GetHubMessageThread returns one conversation's full message history.
|
||||
func GetHubMessageThread(c *fiber.Ctx) error {
|
||||
hubID := c.Locals("hubid").(int)
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid conversation id")
|
||||
}
|
||||
|
||||
var conv models.HubConversation
|
||||
if err := db.DB.Where("hubconversationid = ? AND hubid = ?", id, hubID).First(&conv).Error; err != nil {
|
||||
return utils.NotFound(c, "conversation not found")
|
||||
}
|
||||
|
||||
var messages []models.HubMessage
|
||||
db.DB.Where("hubconversationid = ?", id).Order("createdat ASC").Find(&messages)
|
||||
|
||||
msgResponse := make([]fiber.Map, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
msgResponse = append(msgResponse, fiber.Map{
|
||||
"sender": m.Sender,
|
||||
"text": m.Messagetext,
|
||||
"time": m.Createdat.Local().Format("3:04 PM"),
|
||||
})
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"id": conv.Hubconversationid,
|
||||
"name": conv.Participantname,
|
||||
"messages": msgResponse,
|
||||
})
|
||||
}
|
||||
|
||||
// SendHubMessage posts a reply from the logged-in hub staff into a conversation.
|
||||
func SendHubMessage(c *fiber.Ctx) error {
|
||||
hubID := c.Locals("hubid").(int)
|
||||
staffID := c.Locals("userid").(int)
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid conversation id")
|
||||
}
|
||||
|
||||
type SendMessageRequest struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
req := new(SendMessageRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if strings.TrimSpace(req.Text) == "" {
|
||||
return utils.BadRequest(c, "text is required")
|
||||
}
|
||||
|
||||
var conv models.HubConversation
|
||||
if err := db.DB.Where("hubconversationid = ? AND hubid = ?", id, hubID).First(&conv).Error; err != nil {
|
||||
return utils.NotFound(c, "conversation not found")
|
||||
}
|
||||
|
||||
message := models.HubMessage{
|
||||
Hubconversationid: id,
|
||||
Sender: "me",
|
||||
Senderstaffid: &staffID,
|
||||
Messagetext: req.Text,
|
||||
Isread: true,
|
||||
}
|
||||
if err := db.DB.Create(&message).Error; err != nil {
|
||||
return utils.Internal(c, "failed to send message")
|
||||
}
|
||||
|
||||
conv.Updatedat = time.Now()
|
||||
db.DB.Save(&conv)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"sender": "me",
|
||||
"text": message.Messagetext,
|
||||
"time": message.Createdat.Local().Format("3:04 PM"),
|
||||
})
|
||||
}
|
||||
|
||||
// MarkHubMessagesRead clears the unread flag on every inbound ("them")
|
||||
// message in a conversation.
|
||||
func MarkHubMessagesRead(c *fiber.Ctx) error {
|
||||
hubID := c.Locals("hubid").(int)
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid conversation id")
|
||||
}
|
||||
|
||||
var conv models.HubConversation
|
||||
if err := db.DB.Where("hubconversationid = ? AND hubid = ?", id, hubID).First(&conv).Error; err != nil {
|
||||
return utils.NotFound(c, "conversation not found")
|
||||
}
|
||||
|
||||
db.DB.Model(&models.HubMessage{}).
|
||||
Where("hubconversationid = ? AND sender = 'them' AND isread = false", id).
|
||||
Update("isread", true)
|
||||
|
||||
return utils.Message(c, "messages marked as read")
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// HUB MANAGEMENT (Doormile staff only)
|
||||
// --------------------
|
||||
@@ -1317,7 +1502,7 @@ func buildMilerRoute(mp models.MilerProfile) fiber.Map {
|
||||
|
||||
coords = append(coords, [2]float64{booking.Pickuplatitude, booking.Pickuplongitude})
|
||||
|
||||
stops = append(stops, fiber.Map{
|
||||
stop := fiber.Map{
|
||||
"seq": i + 1,
|
||||
"address": booking.Pickupaddress,
|
||||
"lat": booking.Pickuplatitude,
|
||||
@@ -1326,7 +1511,30 @@ func buildMilerRoute(mp models.MilerProfile) fiber.Map {
|
||||
"bookingid": booking.Bookingid,
|
||||
"status": status,
|
||||
"eta_minutes": etaMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// Optional enrichment: only set when real data backs it, so the
|
||||
// frontend's "hide when absent" handling never sees a fake zero.
|
||||
if itemCount == 1 {
|
||||
stop["weight"] = fmt.Sprintf("%.1f kg", parcel.Weight)
|
||||
}
|
||||
var payment models.BookingPayment
|
||||
if db.DB.Where("bookingid = ?", a.Bookingid).First(&payment).Error == nil && payment.Amount > 0 {
|
||||
stop["cod"] = payment.Amount
|
||||
}
|
||||
if booking.Preferredpickupfrom != nil && booking.Preferredpickupto != nil {
|
||||
stop["timeslot"] = fmt.Sprintf("%s–%s",
|
||||
booking.Preferredpickupfrom.Format("15"), booking.Preferredpickupto.Format("15"))
|
||||
}
|
||||
if booking.Notes != "" {
|
||||
stop["instructions"] = booking.Notes
|
||||
}
|
||||
if n := len(coords); n > 1 {
|
||||
leg := haversineKM(coords[n-2][0], coords[n-2][1], coords[n-1][0], coords[n-1][1])
|
||||
stop["legdistance_km"] = math.Round(leg*10) / 10
|
||||
}
|
||||
|
||||
stops = append(stops, stop)
|
||||
}
|
||||
|
||||
totalDistance := 0.0
|
||||
@@ -1514,3 +1722,251 @@ func HubAutoAssign(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// HUB REPORT EXPORT
|
||||
// --------------------
|
||||
|
||||
// reportActivityLimit caps the activity timeline included in a report so a
|
||||
// wide date range can't return an unbounded result set.
|
||||
const reportActivityLimit = 500
|
||||
|
||||
// GetHubReport builds the data backing the dashboard's Export button: a
|
||||
// summary plus per-section breakdowns, each filtered to [from, to] (inclusive
|
||||
// day range) and scoped to the requesting hub.
|
||||
func GetHubReport(c *fiber.Ctx) error {
|
||||
hubID := c.Locals("hubid").(int)
|
||||
|
||||
fromStr := c.Query("from")
|
||||
toStr := c.Query("to")
|
||||
if fromStr == "" || toStr == "" {
|
||||
return utils.BadRequest(c, "from and to query params are required (YYYY-MM-DD)")
|
||||
}
|
||||
|
||||
from, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid from date, expected YYYY-MM-DD")
|
||||
}
|
||||
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid to date, expected YYYY-MM-DD")
|
||||
}
|
||||
to := toDate.Add(24*time.Hour - time.Nanosecond)
|
||||
if to.Before(from) {
|
||||
return utils.BadRequest(c, "to date must not be before from date")
|
||||
}
|
||||
|
||||
// ---- summary ----
|
||||
var parcelsReceived int64
|
||||
db.DB.Model(&models.ConsignmentHistory{}).
|
||||
Where("hubid = ? AND eventstatus = ? AND createdat BETWEEN ? AND ?", hubID, constants.ConsignmentInwardedAtHub, from, to).
|
||||
Count(&parcelsReceived)
|
||||
|
||||
var batchesDispatched int64
|
||||
db.DB.Model(&models.Tripsheet{}).
|
||||
Where("sourcehubid = ? AND dispatchtime BETWEEN ? AND ? AND deletedat IS NULL", hubID, from, to).
|
||||
Count(&batchesDispatched)
|
||||
|
||||
var ordersAssigned int64
|
||||
db.DB.Model(&models.BookingAssignment{}).
|
||||
Joins("JOIN milerprofiles mp ON mp.userid = bookingassignments.mileruserid").
|
||||
Where("mp.hubid = ? AND bookingassignments.assignedat BETWEEN ? AND ?", hubID, from, to).
|
||||
Count(&ordersAssigned)
|
||||
|
||||
var exceptionsCount int64
|
||||
db.DB.Model(&models.ConsignmentException{}).
|
||||
Where("hubid = ? AND createdat BETWEEN ? AND ?", hubID, from, to).
|
||||
Count(&exceptionsCount)
|
||||
|
||||
var codCollected float64
|
||||
db.DB.Model(&models.BookingPayment{}).
|
||||
Joins("JOIN milerprofiles mp ON mp.userid = bookingpayments.collectedbyuserid").
|
||||
Where("mp.hubid = ? AND bookingpayments.paymentstatus = ? AND bookingpayments.createdat BETWEEN ? AND ?",
|
||||
hubID, constants.PaymentStatusPaid, from, to).
|
||||
Select("COALESCE(SUM(bookingpayments.amount), 0)").Scan(&codCollected)
|
||||
|
||||
// ---- inbound: sourced from consignmenthistory, not consignments.status,
|
||||
// so a parcel that has since moved past "Inwarded_at_Hub" still shows up
|
||||
// for the day it actually arrived. ----
|
||||
var inboundHistory []models.ConsignmentHistory
|
||||
db.DB.Where("hubid = ? AND eventstatus = ? AND createdat BETWEEN ? AND ?", hubID, constants.ConsignmentInwardedAtHub, from, to).
|
||||
Order("createdat ASC").Find(&inboundHistory)
|
||||
|
||||
inbound := make([]fiber.Map, 0, len(inboundHistory))
|
||||
for _, h := range inboundHistory {
|
||||
var cs models.Consignment
|
||||
if db.DB.Where("consignmentid = ?", h.Consignmentid).First(&cs).Error != nil {
|
||||
continue
|
||||
}
|
||||
originName := fmt.Sprintf("Direct pickup (%s)", cs.Pickuppincode)
|
||||
if cs.Originhubid != nil {
|
||||
var oh models.Hub
|
||||
if db.DB.Where("hubid = ?", *cs.Originhubid).First(&oh).Error == nil {
|
||||
originName = oh.Hubname
|
||||
}
|
||||
}
|
||||
inbound = append(inbound, fiber.Map{
|
||||
"date": h.Createdat.Format("2006-01-02"),
|
||||
// no FK from consignments back to a customer/booking record
|
||||
"bookingid": nil,
|
||||
"trackingnumber": cs.Trackingno,
|
||||
"sendername": "",
|
||||
"origin": originName,
|
||||
"destination": cs.Deliverypincode,
|
||||
"weight": fmt.Sprintf("%.1f kg", cs.Chargeableweight),
|
||||
"condition": cs.Condition,
|
||||
"shelf": cs.Shelf,
|
||||
"inboundedat": h.Createdat,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- dispatch ----
|
||||
var tripsheets []models.Tripsheet
|
||||
db.DB.Where("sourcehubid = ? AND dispatchtime BETWEEN ? AND ? AND deletedat IS NULL", hubID, from, to).
|
||||
Order("dispatchtime ASC").Find(&tripsheets)
|
||||
|
||||
dispatch := make([]fiber.Map, 0, len(tripsheets))
|
||||
for _, ts := range tripsheets {
|
||||
var vehicleNo string
|
||||
if ts.Vehicleid != nil {
|
||||
var v models.Vehicle
|
||||
if db.DB.Where("vehicleid = ?", *ts.Vehicleid).First(&v).Error == nil {
|
||||
vehicleNo = v.Vehicleno
|
||||
}
|
||||
}
|
||||
var itemCount int64
|
||||
db.DB.Model(&models.TripsheetItem{}).Where("tripsheetid = ?", ts.Tripsheetid).Count(&itemCount)
|
||||
|
||||
date := ""
|
||||
if ts.Dispatchtime != nil {
|
||||
date = ts.Dispatchtime.Format("2006-01-02")
|
||||
}
|
||||
|
||||
dispatch = append(dispatch, fiber.Map{
|
||||
"date": date,
|
||||
"batchlabel": ts.Batchlabel,
|
||||
"kind": ts.Batchkind,
|
||||
"destination": ts.Destinationlabel,
|
||||
"vehicle": vehicleNo,
|
||||
"itemcount": itemCount,
|
||||
"status": ts.Status,
|
||||
"dispatchtime": ts.Dispatchtime,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- assignments ----
|
||||
var assignments []models.BookingAssignment
|
||||
db.DB.Joins("JOIN milerprofiles mp ON mp.userid = bookingassignments.mileruserid").
|
||||
Where("mp.hubid = ? AND bookingassignments.assignedat BETWEEN ? AND ?", hubID, from, to).
|
||||
Order("bookingassignments.assignedat ASC").Find(&assignments)
|
||||
|
||||
assignmentRows := make([]fiber.Map, 0, len(assignments))
|
||||
for _, a := range assignments {
|
||||
var booking models.PickupBooking
|
||||
if db.DB.Where("bookingid = ?", a.Bookingid).First(&booking).Error != nil {
|
||||
continue
|
||||
}
|
||||
var customer models.AppCustomer
|
||||
db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer)
|
||||
var miler models.MilerProfile
|
||||
db.DB.Where("userid = ?", a.Mileruserid).First(&miler)
|
||||
|
||||
assignmentRows = append(assignmentRows, fiber.Map{
|
||||
"date": a.Assignedat.Format("2006-01-02"),
|
||||
"bookingid": a.Bookingid,
|
||||
"customer": strings.TrimSpace(customer.Firstname + " " + customer.Lastname),
|
||||
"milername": miler.Displayname,
|
||||
"pickup": zoneName(booking.Pickuppincode),
|
||||
"delivery": booking.Deliverycity,
|
||||
"status": booking.Status,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- exceptions ----
|
||||
var exceptions []models.ConsignmentException
|
||||
db.DB.Where("hubid = ? AND createdat BETWEEN ? AND ?", hubID, from, to).
|
||||
Order("createdat ASC").Find(&exceptions)
|
||||
|
||||
exceptionRows := make([]fiber.Map, 0, len(exceptions))
|
||||
for _, ex := range exceptions {
|
||||
var cs models.Consignment
|
||||
db.DB.Where("consignmentid = ?", ex.Consignmentid).First(&cs)
|
||||
|
||||
reason := ex.Description
|
||||
if reason == "" {
|
||||
reason = ex.Exceptiontype
|
||||
}
|
||||
|
||||
exceptionRows = append(exceptionRows, fiber.Map{
|
||||
"date": ex.Createdat.Format("2006-01-02"),
|
||||
"trackingnumber": cs.Trackingno,
|
||||
"reason": reason,
|
||||
"shelf": cs.Shelf,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- activity: same 4-source union as GetHubActivity, windowed to the
|
||||
// report's date range instead of "today". ----
|
||||
activityQuery := `
|
||||
(SELECT c.updatedat AS time, 'inbound' AS type,
|
||||
('Received parcel ' || c.trackingno || ' from ' || COALESCE(oh.hubname, 'Direct Pickup')) AS text
|
||||
FROM consignments c
|
||||
LEFT JOIN hubs oh ON c.originhubid = oh.hubid
|
||||
WHERE c.currenthubid = ? AND c.status = ? AND c.updatedat BETWEEN ? AND ?)
|
||||
UNION ALL
|
||||
(SELECT t.dispatchtime AS time, 'dispatch' AS type,
|
||||
('Dispatched batch ' || t.tripsheetno || ' to ' || COALESCE(t.destinationlabel, '')) AS text
|
||||
FROM tripsheets t
|
||||
WHERE t.sourcehubid = ? AND t.dispatchtime BETWEEN ? AND ?)
|
||||
UNION ALL
|
||||
(SELECT ce.createdat AS time, 'exception' AS type,
|
||||
('Exception raised: ' || ce.exceptiontype || ' on ' || c.trackingno) AS text
|
||||
FROM consignmentexceptions ce
|
||||
JOIN consignments c ON ce.consignmentid = c.consignmentid
|
||||
WHERE ce.hubid = ? AND ce.createdat BETWEEN ? AND ?)
|
||||
UNION ALL
|
||||
(SELECT ba.assignedat AS time, 'sorting' AS type,
|
||||
('Assigned booking #' || ba.bookingid || ' to miler ' || mp.displayname) AS text
|
||||
FROM bookingassignments ba
|
||||
JOIN milerprofiles mp ON ba.mileruserid = mp.userid
|
||||
WHERE mp.hubid = ? AND ba.assignedat BETWEEN ? AND ?)
|
||||
ORDER BY time ASC
|
||||
LIMIT ?
|
||||
`
|
||||
var activityEntries []activityEntry
|
||||
db.DB.Raw(activityQuery,
|
||||
hubID, constants.ConsignmentInwardedAtHub, from, to,
|
||||
hubID, from, to,
|
||||
hubID, from, to,
|
||||
hubID, from, to,
|
||||
reportActivityLimit,
|
||||
).Scan(&activityEntries)
|
||||
|
||||
activity := make([]fiber.Map, 0, len(activityEntries))
|
||||
for _, e := range activityEntries {
|
||||
activity = append(activity, fiber.Map{
|
||||
"time": e.Time,
|
||||
"type": e.Type,
|
||||
"text": e.Text,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"range": fiber.Map{
|
||||
"from": fromStr,
|
||||
"to": toStr,
|
||||
},
|
||||
"summary": fiber.Map{
|
||||
"parcels_received": parcelsReceived,
|
||||
"batches_dispatched": batchesDispatched,
|
||||
"orders_assigned": ordersAssigned,
|
||||
"exceptions": exceptionsCount,
|
||||
"cod_collected": codCollected,
|
||||
},
|
||||
"inbound": inbound,
|
||||
"dispatch": dispatch,
|
||||
"assignments": assignmentRows,
|
||||
"exceptions": exceptionRows,
|
||||
"activity": activity,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user