Add technical check history
A car's mandatory roadworthiness inspections — przegląd techniczny, MOT, TÜV — had nowhere to live. Log them behind Service history, which this mirrors minus the odometer: an inspection falls due on a date whatever the mileage reads. The cycle is legal rather than mechanical, and it is not constant — a new car's first check falls due years later than its third. So the car's technical_check_interval_days only prefills the next date, and any check overrides it with the valid-until printed on its own certificate. A failed check derives no next date at all: reading one off a failure would stamp a reassuring "valid until" on a car that just flunked. The next-due date and its status are derived on every read rather than stored, for the same reason documents are — a stored verdict goes stale as the date passes. The response carries the same expiry shape documents use, so the web app reuses the existing badge rather than growing a second one. Each check records result, cost, station and notes, and holds the certificate as its single attachment on the same terms as every other record. Needs `node scripts/setup-pocketbase.mjs` to create technical_checks and add the interval to cars. The reformatting in records.go is gofmt realigning the car struct around a longer field name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
03738f08dc
commit
21c99ad762
@@ -232,4 +232,7 @@ func applyCarDefaults(c *models.Car) {
|
||||
if c.ServiceIntervalKm <= 0 {
|
||||
c.ServiceIntervalKm = 15000
|
||||
}
|
||||
if c.TechnicalCheckIntervalDays <= 0 {
|
||||
c.TechnicalCheckIntervalDays = models.DefaultTechnicalCheckIntervalDays
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,78 +41,81 @@ func formatPBDate(t time.Time) string {
|
||||
|
||||
// carRecord is the PocketBase-facing shape of a car (snake_case fields).
|
||||
type carRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Make string `json:"make"`
|
||||
Model string `json:"model"`
|
||||
Year int `json:"year"`
|
||||
Registration string `json:"registration"`
|
||||
RegistrationCountry string `json:"registration_country"`
|
||||
VIN string `json:"vin"`
|
||||
ServiceIntervalDays int `json:"service_interval_days"`
|
||||
ServiceIntervalKm int `json:"service_interval_km"`
|
||||
OilSpec string `json:"oil_spec"`
|
||||
TransmissionOilSpec string `json:"transmission_oil_spec"`
|
||||
DifferentialOilSpec string `json:"differential_oil_spec"`
|
||||
BrakeFluidSpec string `json:"brake_fluid_spec"`
|
||||
CoolantSpec string `json:"coolant_spec"`
|
||||
CurrentKm int `json:"current_km"`
|
||||
FuelType string `json:"fuel_type"`
|
||||
BuildDate string `json:"build_date"`
|
||||
FirstRegistrationDate string `json:"first_registration_date"`
|
||||
Owner string `json:"owner"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Make string `json:"make"`
|
||||
Model string `json:"model"`
|
||||
Year int `json:"year"`
|
||||
Registration string `json:"registration"`
|
||||
RegistrationCountry string `json:"registration_country"`
|
||||
VIN string `json:"vin"`
|
||||
ServiceIntervalDays int `json:"service_interval_days"`
|
||||
ServiceIntervalKm int `json:"service_interval_km"`
|
||||
TechnicalCheckIntervalDays int `json:"technical_check_interval_days"`
|
||||
OilSpec string `json:"oil_spec"`
|
||||
TransmissionOilSpec string `json:"transmission_oil_spec"`
|
||||
DifferentialOilSpec string `json:"differential_oil_spec"`
|
||||
BrakeFluidSpec string `json:"brake_fluid_spec"`
|
||||
CoolantSpec string `json:"coolant_spec"`
|
||||
CurrentKm int `json:"current_km"`
|
||||
FuelType string `json:"fuel_type"`
|
||||
BuildDate string `json:"build_date"`
|
||||
FirstRegistrationDate string `json:"first_registration_date"`
|
||||
Owner string `json:"owner"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
func (rec carRecord) toModel() models.Car {
|
||||
return models.Car{
|
||||
ID: rec.ID,
|
||||
Name: rec.Name,
|
||||
Make: rec.Make,
|
||||
Model: rec.Model,
|
||||
Year: rec.Year,
|
||||
Registration: rec.Registration,
|
||||
RegistrationCountry: rec.RegistrationCountry,
|
||||
VIN: rec.VIN,
|
||||
ServiceIntervalDays: rec.ServiceIntervalDays,
|
||||
ServiceIntervalKm: rec.ServiceIntervalKm,
|
||||
OilSpec: rec.OilSpec,
|
||||
TransmissionOilSpec: rec.TransmissionOilSpec,
|
||||
DifferentialOilSpec: rec.DifferentialOilSpec,
|
||||
BrakeFluidSpec: rec.BrakeFluidSpec,
|
||||
CoolantSpec: rec.CoolantSpec,
|
||||
CurrentKm: rec.CurrentKm,
|
||||
FuelType: rec.FuelType,
|
||||
BuildDate: rec.BuildDate,
|
||||
FirstRegistrationDate: rec.FirstRegistrationDate,
|
||||
Owner: rec.Owner,
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
ID: rec.ID,
|
||||
Name: rec.Name,
|
||||
Make: rec.Make,
|
||||
Model: rec.Model,
|
||||
Year: rec.Year,
|
||||
Registration: rec.Registration,
|
||||
RegistrationCountry: rec.RegistrationCountry,
|
||||
VIN: rec.VIN,
|
||||
ServiceIntervalDays: rec.ServiceIntervalDays,
|
||||
ServiceIntervalKm: rec.ServiceIntervalKm,
|
||||
TechnicalCheckIntervalDays: rec.TechnicalCheckIntervalDays,
|
||||
OilSpec: rec.OilSpec,
|
||||
TransmissionOilSpec: rec.TransmissionOilSpec,
|
||||
DifferentialOilSpec: rec.DifferentialOilSpec,
|
||||
BrakeFluidSpec: rec.BrakeFluidSpec,
|
||||
CoolantSpec: rec.CoolantSpec,
|
||||
CurrentKm: rec.CurrentKm,
|
||||
FuelType: rec.FuelType,
|
||||
BuildDate: rec.BuildDate,
|
||||
FirstRegistrationDate: rec.FirstRegistrationDate,
|
||||
Owner: rec.Owner,
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// carPayload builds the write payload for create/update from a domain Car.
|
||||
func carPayload(c models.Car) map[string]any {
|
||||
return map[string]any{
|
||||
"name": c.Name,
|
||||
"make": c.Make,
|
||||
"model": c.Model,
|
||||
"year": c.Year,
|
||||
"registration": c.Registration,
|
||||
"registration_country": c.RegistrationCountry,
|
||||
"vin": c.VIN,
|
||||
"service_interval_days": c.ServiceIntervalDays,
|
||||
"service_interval_km": c.ServiceIntervalKm,
|
||||
"oil_spec": c.OilSpec,
|
||||
"transmission_oil_spec": c.TransmissionOilSpec,
|
||||
"differential_oil_spec": c.DifferentialOilSpec,
|
||||
"brake_fluid_spec": c.BrakeFluidSpec,
|
||||
"coolant_spec": c.CoolantSpec,
|
||||
"current_km": c.CurrentKm,
|
||||
"fuel_type": c.FuelType,
|
||||
"build_date": c.BuildDate,
|
||||
"first_registration_date": c.FirstRegistrationDate,
|
||||
"name": c.Name,
|
||||
"make": c.Make,
|
||||
"model": c.Model,
|
||||
"year": c.Year,
|
||||
"registration": c.Registration,
|
||||
"registration_country": c.RegistrationCountry,
|
||||
"vin": c.VIN,
|
||||
"service_interval_days": c.ServiceIntervalDays,
|
||||
"service_interval_km": c.ServiceIntervalKm,
|
||||
"technical_check_interval_days": c.TechnicalCheckIntervalDays,
|
||||
"oil_spec": c.OilSpec,
|
||||
"transmission_oil_spec": c.TransmissionOilSpec,
|
||||
"differential_oil_spec": c.DifferentialOilSpec,
|
||||
"brake_fluid_spec": c.BrakeFluidSpec,
|
||||
"coolant_spec": c.CoolantSpec,
|
||||
"current_km": c.CurrentKm,
|
||||
"fuel_type": c.FuelType,
|
||||
"build_date": c.BuildDate,
|
||||
"first_registration_date": c.FirstRegistrationDate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +164,51 @@ func servicePayload(r models.ServiceRecord) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// --- technical checks ---
|
||||
|
||||
type technicalCheckRecord struct {
|
||||
ID string `json:"id"`
|
||||
Car string `json:"car"`
|
||||
Date string `json:"date"`
|
||||
Result string `json:"result"`
|
||||
Cost float64 `json:"cost"`
|
||||
Station string `json:"station"`
|
||||
ValidUntil string `json:"valid_until"`
|
||||
Notes string `json:"notes"`
|
||||
File string `json:"file"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
func (rec technicalCheckRecord) toModel() models.TechnicalCheck {
|
||||
return models.TechnicalCheck{
|
||||
ID: rec.ID,
|
||||
Car: rec.Car,
|
||||
Date: parsePBDate(rec.Date),
|
||||
Result: rec.Result,
|
||||
Cost: rec.Cost,
|
||||
Station: rec.Station,
|
||||
ValidUntil: parsePBDatePtr(rec.ValidUntil),
|
||||
Notes: rec.Notes,
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// technicalCheckPayload omits the file field — see attachmentOf.
|
||||
func technicalCheckPayload(t models.TechnicalCheck) map[string]any {
|
||||
return map[string]any{
|
||||
"car": t.Car,
|
||||
"date": formatPBDate(t.Date),
|
||||
"result": t.Result,
|
||||
"cost": t.Cost,
|
||||
"station": t.Station,
|
||||
"valid_until": formatPBDatePtr(t.ValidUntil),
|
||||
"notes": t.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
// --- attachments ---
|
||||
|
||||
// attachmentOf renders a PocketBase file field into the model's attachment pair.
|
||||
|
||||
@@ -54,6 +54,12 @@
|
||||
// GET /api/parts POST /api/parts
|
||||
// GET /api/parts/{id} PATCH /api/parts/{id} DELETE /api/parts/{id}
|
||||
//
|
||||
// # technical checks (roadworthiness inspections; time-only, next date derived)
|
||||
// GET /api/cars/{id}/technical-checks
|
||||
// GET /api/technical-checks POST /api/technical-checks
|
||||
// GET /api/technical-checks/{id} PATCH /api/technical-checks/{id}
|
||||
// DELETE /api/technical-checks/{id}
|
||||
//
|
||||
// # fuel tracking (efficiency is derived on read, never stored)
|
||||
// GET /api/cars/{id}/fuel-entries
|
||||
// GET /api/cars/{id}/fuel-stats
|
||||
@@ -74,7 +80,8 @@
|
||||
// DELETE /api/car-documents/{id}
|
||||
//
|
||||
// # attachments — one optional file per record, same three verbs everywhere.
|
||||
// # {records} is car-documents | service-records | maintenance | fuel-entries | parts
|
||||
// # {records} is car-documents | service-records | technical-checks | maintenance
|
||||
// # | fuel-entries | parts
|
||||
// POST /api/{records}/{id}/file
|
||||
// GET /api/{records}/{id}/file
|
||||
// DELETE /api/{records}/{id}/file
|
||||
@@ -101,15 +108,16 @@ import (
|
||||
|
||||
// PocketBase collection names.
|
||||
const (
|
||||
colCars = "cars"
|
||||
colServices = "service_records"
|
||||
colParts = "parts"
|
||||
colShares = "car_shares"
|
||||
colOrgs = "organizations"
|
||||
colFuel = "fuel_entries"
|
||||
colMaintenance = "maintenance_entries"
|
||||
colDocuments = "car_documents"
|
||||
colReminders = "reminders"
|
||||
colCars = "cars"
|
||||
colServices = "service_records"
|
||||
colTechnicalChecks = "technical_checks"
|
||||
colParts = "parts"
|
||||
colShares = "car_shares"
|
||||
colOrgs = "organizations"
|
||||
colFuel = "fuel_entries"
|
||||
colMaintenance = "maintenance_entries"
|
||||
colDocuments = "car_documents"
|
||||
colReminders = "reminders"
|
||||
)
|
||||
|
||||
// Server wires together the HTTP handlers and their dependencies.
|
||||
@@ -241,6 +249,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("PATCH /api/cars/{id}", s.updateCar)
|
||||
mux.HandleFunc("DELETE /api/cars/{id}", s.deleteCar)
|
||||
mux.HandleFunc("GET /api/cars/{id}/service-records", s.listCarServiceRecords)
|
||||
mux.HandleFunc("GET /api/cars/{id}/technical-checks", s.listCarTechnicalChecks)
|
||||
mux.HandleFunc("GET /api/cars/{id}/parts", s.listCarParts)
|
||||
mux.HandleFunc("GET /api/cars/{id}/fuel-entries", s.listCarFuelEntries)
|
||||
mux.HandleFunc("GET /api/cars/{id}/fuel-stats", s.listCarFuelStats)
|
||||
@@ -258,6 +267,13 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("PATCH /api/service-records/{id}", s.updateServiceRecord)
|
||||
mux.HandleFunc("DELETE /api/service-records/{id}", s.deleteServiceRecord)
|
||||
|
||||
// Technical checks (roadworthiness inspections).
|
||||
mux.HandleFunc("GET /api/technical-checks", s.listTechnicalChecks)
|
||||
mux.HandleFunc("POST /api/technical-checks", s.createTechnicalCheck)
|
||||
mux.HandleFunc("GET /api/technical-checks/{id}", s.getTechnicalCheck)
|
||||
mux.HandleFunc("PATCH /api/technical-checks/{id}", s.updateTechnicalCheck)
|
||||
mux.HandleFunc("DELETE /api/technical-checks/{id}", s.deleteTechnicalCheck)
|
||||
|
||||
// Parts.
|
||||
mux.HandleFunc("GET /api/parts", s.listParts)
|
||||
mux.HandleFunc("POST /api/parts", s.createPart)
|
||||
@@ -300,6 +316,7 @@ func (s *Server) Handler() http.Handler {
|
||||
// alongside is what renders the record after an upload.
|
||||
s.attachmentRoutes(mux, "/api/car-documents", colDocuments, s.getDocument)
|
||||
s.attachmentRoutes(mux, "/api/service-records", colServices, s.getServiceRecord)
|
||||
s.attachmentRoutes(mux, "/api/technical-checks", colTechnicalChecks, s.getTechnicalCheck)
|
||||
s.attachmentRoutes(mux, "/api/maintenance", colMaintenance, s.getMaintenance)
|
||||
s.attachmentRoutes(mux, "/api/fuel-entries", colFuel, s.getFuelEntry)
|
||||
s.attachmentRoutes(mux, "/api/parts", colParts, s.getPart)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"drivervault/apiserver/internal/models"
|
||||
)
|
||||
|
||||
// Technical check history: the mandatory roadworthiness inspections a car has
|
||||
// been through — przegląd techniczny, MOT, TÜV.
|
||||
//
|
||||
// Shaped like the service log next to it, but time-only: an inspection falls due
|
||||
// on a date regardless of the odometer. The next-due date is assessed live on
|
||||
// every read (models.TechnicalCheck.ComputeTechnicalCheckDerived) rather than
|
||||
// stored, for the same reason documents are — a lapsed certificate is a car that
|
||||
// cannot legally be driven, and a stored verdict would quietly go stale.
|
||||
//
|
||||
// The certificate scan is handled by attachments.go, on the same terms as every
|
||||
// other record's attachment.
|
||||
|
||||
var technicalCheckResults = map[string]bool{"passed": true, "failed": true}
|
||||
|
||||
// listCarTechnicalChecks serves GET /api/cars/{id}/technical-checks.
|
||||
func (s *Server) listCarTechnicalChecks(w http.ResponseWriter, r *http.Request) {
|
||||
carID := r.PathValue("id")
|
||||
if !s.requireCarAccess(w, r, carID, accessRead) {
|
||||
return
|
||||
}
|
||||
s.respondTechnicalCheckList(w, r, carID)
|
||||
}
|
||||
|
||||
// listTechnicalChecks serves GET /api/technical-checks?car={id}.
|
||||
func (s *Server) listTechnicalChecks(w http.ResponseWriter, r *http.Request) {
|
||||
carID := r.URL.Query().Get("car")
|
||||
if !s.requireCarAccess(w, r, carID, accessRead) {
|
||||
return
|
||||
}
|
||||
s.respondTechnicalCheckList(w, r, carID)
|
||||
}
|
||||
|
||||
func (s *Server) respondTechnicalCheckList(w http.ResponseWriter, r *http.Request, carID string) {
|
||||
q := url.Values{}
|
||||
q.Set("sort", "-date") // most-recent check first, like the service log
|
||||
q.Set("perPage", "500")
|
||||
q.Set("filter", fmt.Sprintf("car='%s'", carID))
|
||||
|
||||
res, err := s.pb.List(r.Context(), colTechnicalChecks, q)
|
||||
if err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
var recs []technicalCheckRecord
|
||||
if err := json.Unmarshal(res.Items, &recs); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cars := newCarCache(s)
|
||||
now := time.Now()
|
||||
out := make([]models.TechnicalCheck, 0, len(recs))
|
||||
for _, rec := range recs {
|
||||
m := rec.toModel()
|
||||
car, err := cars.get(r.Context(), m.Car)
|
||||
if err != nil {
|
||||
car = nil // fall back to the default interval rather than dropping the row
|
||||
}
|
||||
m.ComputeTechnicalCheckDerived(car, now)
|
||||
out = append(out, m)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) getTechnicalCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var rec technicalCheckRecord
|
||||
if err := s.pb.GetOne(r.Context(), colTechnicalChecks, r.PathValue("id"), &rec); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
m := rec.toModel()
|
||||
if !s.requireCarAccess(w, r, m.Car, accessRead) {
|
||||
return
|
||||
}
|
||||
s.writeTechnicalCheck(w, r, m, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) createTechnicalCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var in models.TechnicalCheck
|
||||
if err := decodeJSON(r, &in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if in.Result == "" {
|
||||
in.Result = "passed"
|
||||
}
|
||||
if msg := validateTechnicalCheck(in); msg != "" {
|
||||
writeError(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
if !s.requireCarAccess(w, r, in.Car, accessWrite) {
|
||||
return
|
||||
}
|
||||
|
||||
var rec technicalCheckRecord
|
||||
if err := s.pb.Create(r.Context(), colTechnicalChecks, technicalCheckPayload(in), &rec); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeTechnicalCheck(w, r, rec.toModel(), http.StatusCreated)
|
||||
}
|
||||
|
||||
func (s *Server) updateTechnicalCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var in models.TechnicalCheck
|
||||
if err := decodeJSON(r, &in); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Enforce write access via the record's existing parent car, so the body's
|
||||
// car field cannot be used to escape into another user's car.
|
||||
var existing technicalCheckRecord
|
||||
if err := s.pb.GetOne(r.Context(), colTechnicalChecks, r.PathValue("id"), &existing); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
if !s.requireCarAccess(w, r, existing.Car, accessWrite) {
|
||||
return
|
||||
}
|
||||
in.Car = existing.Car // a check's car is not reassignable via PATCH
|
||||
if in.Result == "" {
|
||||
in.Result = "passed"
|
||||
}
|
||||
if msg := validateTechnicalCheck(in); msg != "" {
|
||||
writeError(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var rec technicalCheckRecord
|
||||
if err := s.pb.Update(r.Context(), colTechnicalChecks, r.PathValue("id"), technicalCheckPayload(in), &rec); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeTechnicalCheck(w, r, rec.toModel(), http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *Server) deleteTechnicalCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var existing technicalCheckRecord
|
||||
if err := s.pb.GetOne(r.Context(), colTechnicalChecks, r.PathValue("id"), &existing); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
if !s.requireCarAccess(w, r, existing.Car, accessWrite) {
|
||||
return
|
||||
}
|
||||
if err := s.pb.Delete(r.Context(), colTechnicalChecks, r.PathValue("id")); err != nil {
|
||||
writePBError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// writeTechnicalCheck derives the next-due date against the parent car and
|
||||
// responds. Shared by the three handlers that return a single check.
|
||||
func (s *Server) writeTechnicalCheck(w http.ResponseWriter, r *http.Request, m models.TechnicalCheck, status int) {
|
||||
car, err := s.carModel(r.Context(), m.Car)
|
||||
if err != nil {
|
||||
car = nil
|
||||
}
|
||||
m.ComputeTechnicalCheckDerived(car, time.Now())
|
||||
writeJSON(w, status, m)
|
||||
}
|
||||
|
||||
func validateTechnicalCheck(t models.TechnicalCheck) string {
|
||||
switch {
|
||||
case t.Car == "":
|
||||
return "car is required"
|
||||
case t.Date.IsZero():
|
||||
return "date is required"
|
||||
case !technicalCheckResults[t.Result]:
|
||||
return "result must be passed or failed"
|
||||
case t.Cost < 0:
|
||||
return "cost cannot be negative"
|
||||
case len(strings.TrimSpace(t.Station)) > 200:
|
||||
return "station name is too long"
|
||||
}
|
||||
if t.ValidUntil != nil && !t.ValidUntil.IsZero() && t.ValidUntil.Before(t.Date) {
|
||||
return "valid-until date cannot be before the check date"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user