Show nuisance report status

This commit is contained in:
Eli Ribble 2026-04-08 23:37:00 +00:00
parent 37ce3183ca
commit b2c24a0438
No known key found for this signature in database
10 changed files with 133 additions and 82 deletions

View file

@ -18,20 +18,15 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/platform/email"
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
"github.com/Gleipnir-Technology/nidus-sync/platform/publicreport"
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/rs/zerolog/log"
)
func PublicreportByID(ctx context.Context, report_id string) (*models.PublicreportReport, error) {
report, err := models.PublicreportReports.Query(
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),
).One(ctx, db.PGInstance.BobDB)
if err != nil {
return nil, err
}
return report, nil
func PublicreportByID(ctx context.Context, report_id string) (*types.Report, error) {
return publicreport.Report(ctx, report_id)
}
func PublicreportInvalid(ctx context.Context, user User, report_id string) error {
report, err := reportFromID(ctx, user, report_id)

View file

@ -29,7 +29,7 @@ WHERE i.id IN (1, 2, 3, 4)
GROUP BY i.id;
*/
// Get all the images that belong to the list of report IDs
func loadImagesForReport(ctx context.Context, org_id int32, report_ids []int32) (results map[int32][]types.Image, err error) {
func loadImagesForReport(ctx context.Context, report_ids []int32) (results map[int32][]types.Image, err error) {
rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select(
sm.Columns(
"i.storage_uuid AS uuid",

View file

@ -18,13 +18,14 @@ import (
)
func ReportsForOrganization(ctx context.Context, org_id int32) ([]*types.Report, error) {
query := reportQuery(org_id)
query := reportQuery()
query.Apply(
sm.Where(psql.Quote("publicreport", "report", "organization_id").EQ(psql.Arg(org_id))),
sm.Where(psql.Quote("publicreport", "report", "reviewed").IsNull()),
)
return reportQueryToRows(ctx, org_id, query)
return reportQueryToRows(ctx, query)
}
func reportQueryToRows(ctx context.Context, org_id int32, query bob.BaseQuery[*dialect.SelectQuery]) ([]*types.Report, error) {
func reportQueryToRows(ctx context.Context, query bob.BaseQuery[*dialect.SelectQuery]) ([]*types.Report, error) {
rows, err := bob.All(ctx, db.PGInstance.BobDB, query, scan.StructMapper[types.Report]())
if err != nil {
@ -34,7 +35,7 @@ func reportQueryToRows(ctx context.Context, org_id int32, query bob.BaseQuery[*d
for i, row := range rows {
report_ids[i] = row.ID
}
images_by_id, err := loadImagesForReport(ctx, org_id, report_ids)
images_by_id, err := loadImagesForReport(ctx, report_ids)
if err != nil {
return nil, fmt.Errorf("images for report: %w", err)
}
@ -69,12 +70,27 @@ func reportQueryToRows(ctx context.Context, org_id int32, query bob.BaseQuery[*d
}
return results, nil
}
func Reports(ctx context.Context, org_id int32, ids []int32) ([]*types.Report, error) {
query := reportQuery(org_id)
func Report(ctx context.Context, public_id string) (*types.Report, error) {
query := reportQuery()
query.Apply(
sm.Where(psql.Quote("publicreport", "report", "public_id").EQ(psql.Arg(public_id))),
)
reports, err := reportQueryToRows(ctx, query)
if err != nil {
return nil, fmt.Errorf("query to rows: %w", err)
}
if len(reports) != 1 {
return nil, fmt.Errorf("reports returned: %d", len(reports))
}
return reports[0], nil
}
func Reports(ctx context.Context, org_id int32, ids []int32) ([]*types.Report, error) {
query := reportQuery()
query.Apply(
sm.Where(psql.Quote("publicreport", "report", "organization_id").EQ(psql.Arg(org_id))),
sm.Where(psql.Quote("publicreport", "report", "id").EQ(psql.Any(ids))),
)
return reportQueryToRows(ctx, org_id, query)
return reportQueryToRows(ctx, query)
}
func ReportsForOrganizationCount(ctx context.Context, org_id int32) (uint, error) {
type _Row struct {
@ -92,10 +108,11 @@ func ReportsForOrganizationCount(ctx context.Context, org_id int32) (uint, error
}
return row.Count, nil
}
func reportQuery(org_id int32) bob.BaseQuery[*dialect.SelectQuery] {
func reportQuery() bob.BaseQuery[*dialect.SelectQuery] {
return psql.Select(
sm.Columns(
"address_country AS \"address.country\"",
"address_gid AS \"address.gid\"",
"address_locality AS \"address.locality\"",
"address_number AS \"address.number\"",
"address_postal_code AS \"address.postal_code\"",
@ -106,6 +123,7 @@ func reportQuery(org_id int32) bob.BaseQuery[*dialect.SelectQuery] {
"id",
"COALESCE(ST_Y(location::geometry::geometry(point, 4326)), 0.0) AS \"location.latitude\"",
"COALESCE(ST_X(location::geometry::geometry(point, 4326)), 0.0) AS \"location.longitude\"",
"organization_id",
"public_id",
"report_type",
"reporter_email AS \"reporter.email\"",
@ -114,6 +132,5 @@ func reportQuery(org_id int32) bob.BaseQuery[*dialect.SelectQuery] {
"status",
),
sm.From("publicreport.report"),
sm.Where(psql.Quote("publicreport", "report", "organization_id").EQ(psql.Arg(org_id))),
)
}

View file

@ -13,9 +13,12 @@ type Report struct {
Location *Location `db:"location" json:"location"`
Log []LogEntry `db:"-" json:"log"`
Nuisance *Nuisance `db:"nuisance" json:"nuisance"`
DistrictID *int32 `db:"organization_id" json:"-"`
District *string `db:"-" json:"district"`
PublicID string `db:"public_id" json:"public_id"`
Reporter Contact `db:"reporter" json:"reporter"`
Status string `db:"status" json:"status"`
Type string `db:"report_type" json:"type"`
URI string `db:"-" json:"uri"`
Water *Water `db:"water" json:"water"`
}

View file

@ -20,6 +20,7 @@ type district struct {
Name string `json:"name"`
PhoneOffice string `json:"phone_office"`
Slug string `json:"slug"`
URI string `json:"uri"`
URLLogo string `json:"url_logo"`
URLWebsite string `json:"url_website"`
}
@ -75,10 +76,15 @@ func newDistrict(r *router, org *platform.Organization) (*district, error) {
if err != nil {
return nil, fmt.Errorf("logo url: %w", err)
}
uri, err := r.IDToURI("district.ByIDGet", int(org.ID))
if err != nil {
return nil, nhttp.NewError("district uri: %w", err)
}
return &district{
Name: org.Name(),
PhoneOffice: org.PhoneOffice(),
Slug: slug,
URI: uri,
URLLogo: logo,
URLWebsite: org.Website(),
}, nil

View file

@ -2,7 +2,6 @@ package resource
import (
"context"
"time"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
@ -16,25 +15,13 @@ type publicreportR struct {
router *router
}
type publicreport struct {
Address string `json:"address"`
Created time.Time `json:"created"`
District string `json:"district"`
ID string `json:"id"`
ImageCount int `json:"image_count"`
Location types.Location `json:"location"`
Status string `json:"status"`
Type string `json:"type"`
URI string `json:"uri"`
}
func Publicreport(r *router) *publicreportR {
return &publicreportR{
router: r,
}
}
func (res *publicreportR) ByID(ctx context.Context, r *http.Request, query QueryParams) (*publicreport, *nhttp.ErrorWithStatus) {
func (res *publicreportR) ByID(ctx context.Context, r *http.Request, query QueryParams) (*types.Report, *nhttp.ErrorWithStatus) {
vars := mux.Vars(r)
public_id := vars["id"]
if public_id == "" {
@ -44,27 +31,18 @@ func (res *publicreportR) ByID(ctx context.Context, r *http.Request, query Query
if err != nil {
return nil, nhttp.NewError("get report: %w", err)
}
district_uri, err := res.router.IDToURI("district.ByIDGet", int(report.OrganizationID))
if err != nil {
return nil, nhttp.NewError("district uri: %w", err)
var district_uri string
if report.DistrictID != nil {
district_uri, err = res.router.IDToURI("district.ByIDGet", int(*report.DistrictID))
if err != nil {
return nil, nhttp.NewError("district uri: %w", err)
}
}
uri, err := res.router.IDStrToURI("publicreport.ByIDGet", report.PublicID)
if err != nil {
return nil, nhttp.NewError("uri: %w", err)
}
location := types.Location{
Latitude: report.LocationLatitude.GetOr(0.0),
Longitude: report.LocationLongitude.GetOr(0.0),
}
return &publicreport{
District: district_uri,
ID: report.PublicID,
Address: report.AddressRaw,
Created: report.Created,
ImageCount: len(report.R.Images),
Location: location,
Status: report.Status.String(),
Type: report.ReportType.String(),
URI: uri,
}, nil
report.District = &district_uri
report.URI = uri
return report, nil
}

View file

@ -103,10 +103,10 @@ const updateMarkers = () => {
markerData.location.longitude,
markerData.location.latitude,
]);
marker.setDraggable(markerData.draggable ?? false);
marker.setDraggable(false);
} else {
marker = new maplibregl.Marker({
draggable: markerData.draggable ?? false,
draggable: false,
})
.setLngLat([
markerData.location.longitude,
@ -117,6 +117,7 @@ const updateMarkers = () => {
mapMarkers.value.set(markerData.id, marker);
}
});
frameMarkers();
};
// Frame all markers in view
@ -130,7 +131,7 @@ const frameMarkers = () => {
lat: props.markers[0].location.latitude,
lng: props.markers[0].location.longitude,
},
{ duration: 1000 },
{ duration: 1000, zoom: 15 },
{ isInternalUpdate: true },
);
} else {

View file

@ -13,19 +13,6 @@
margin-bottom: 1rem;
padding-bottom: 0;
}
.map-container {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px;
align-items: center;
justify-content: center;
margin-bottom: 20px;
margin-top: 20px;
}
#map {
width: 100%;
height: 100%;
}
.section-heading {
margin-bottom: 1.5rem;
display: flex;
@ -111,9 +98,15 @@ select.tall {
height: 500px;
margin-bottom: 20px;
margin-top: 20px;
align-items: center;
justify-content: center;
/* Prevent touch scrolling issues */
touch-action: pan-y pinch-zoom;
}
#map {
width: 100%;
height: 100%;
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
@ -131,11 +124,6 @@ select.tall {
border-radius: 5px;
}
}
#map {
width: 100%;
height: 100%;
}
</style>
<template>

View file

@ -1,15 +1,31 @@
<style scoped>
.map-container {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px;
margin-bottom: 20px;
margin-top: 20px;
align-items: center;
justify-content: center;
/* Prevent touch scrolling issues */
touch-action: pan-y pinch-zoom;
}
#map {
width: 100%;
height: 100%;
}
.status-badge {
font-size: 1rem;
}
.timeline {
border-left: 3px solid #dee2e6;
padding-left: 20px;
margin-left: 10px;
}
.timeline-item {
position: relative;
margin-bottom: 25px;
}
.timeline-item:before {
content: "";
position: absolute;
@ -20,17 +36,14 @@
border-radius: 50%;
background-color: #0d6efd;
}
.timeline-date {
font-size: 0.85rem;
color: #6c757d;
}
.status-badge {
font-size: 1rem;
}
</style>
<template>
<HeaderDistrict v-if="district" />
<Header v-else />
<div class="container my-4" v-if="report">
<!-- Report ID and Status Section -->
<div class="card mb-4">
@ -62,7 +75,7 @@
<div class="row">
<div class="col-md-12">
<strong><i class="bi bi-pin-map me-2"></i>Location:</strong>
<span>{{ report.address }}</span>
<span>{{ report.address.raw }}</span>
</div>
</div>
<div class="row">
@ -91,6 +104,29 @@
</div>
</div>
</div>
<!-- History Timeline -->
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-clock-history me-2"></i>Request History
</h5>
</div>
<div class="card-body">
<div class="timeline">
<div
v-for="(item, index) in report.log"
:key="index"
class="timeline-item"
>
<div class="timeline-date">
{{ formatTimeRelative(item.created) }}
</div>
<h5 class="mb-1">{{ item.type }}</h5>
<p class="mb-0">{{ item.message }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="container my-4" v-else>
<p>loading...</p>
@ -99,6 +135,8 @@
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { computedAsync } from "@vueuse/core";
import Header from "@/rmo/components/Header.vue";
import HeaderDistrict from "@/components/HeaderDistrict.vue";
import MapLocatorDisplay from "@/components/MapLocatorDisplay.vue";
import { useStoreDistrict } from "@/rmo/store/district";
import { useStorePublicreport } from "@/store/publicreport";
@ -130,7 +168,7 @@ const markers = computed((): Marker[] => {
}
return [
{
id: report.value.id,
id: props.id,
location: report.value.location,
},
];

View file

@ -32,25 +32,49 @@ export interface Geocode {
cell: number;
location: Location;
}
export interface LogEntryDTO {
created: string;
message: string;
type: string;
user_id: number;
}
export class LogEntry {
constructor(
public created: Date,
public message: string,
public type: string,
public user_id: number,
) {}
static fromJSON(json: LogEntryDTO): LogEntry {
return new LogEntry(
new Date(json.created),
json.message,
json.type,
json.user_id,
);
}
}
export interface PublicreportDTO {
address: string;
address: Address;
created: string;
district: string;
id: string;
image_count: number;
location: Location;
log: LogEntryDTO[];
status: string;
type: string;
uri: string;
}
export class Publicreport {
constructor(
public address: string,
public address: Address,
public created: Date,
public district: string,
public id: string,
public image_count: number,
public location: Location,
public log: LogEntry[],
public status: string,
public type: string,
public uri: string,
@ -63,6 +87,7 @@ export class Publicreport {
json.id,
json.image_count,
json.location,
json.log.map((l: LogEntryDTO) => LogEntry.fromJSON(l)),
json.status,
json.type,
json.uri,