Add attachments to service, maintenance, fuel and parts records
Documents already carried a single optional file: upload, download, detach,
access-checked on every request and proxied through this server, so PocketBase's
files are never public URLs. Service records, workshop visits, refills and
catalog parts all want the same thing — a receipt, an invoice, a photo of the
box — so extend it to them.
Rather than copy the document handlers four more times, lift them into one
shared layer. Every attachable collection has a car relation and a file field,
which is what lets a single set of handlers authorize and serve all of them. An
upload finishes by delegating to the collection's own GET handler, so the
response carries the full record — derived fields and all — exactly as a re-read
would. The web side gets the same treatment: one picker component and one
upload-after-save helper behind all five forms. Net effect is five features for
about the cost of the one that was already there.
Alongside:
- parts gain a notes field
- Reminders moves behind Parts catalog in the car detail tabs
- the changed-part labels spell out in full ("Oil & Oil filter" rather than
"Oil & filter"), and the form and table now agree
The PocketBase schema must be migrated before the new attachments work:
scripts/setup-pocketbase.mjs adds the file fields and parts.notes. It is
additive and safe to re-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e64c89a564
commit
07192f1238
@@ -127,6 +127,7 @@ type serviceRecord struct {
|
||||
ChangedEngineAirFilter bool `json:"changed_engine_air_filter"`
|
||||
ChangedCabinAirFilter bool `json:"changed_cabin_air_filter"`
|
||||
Notes string `json:"notes"`
|
||||
File string `json:"file"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
@@ -141,11 +142,13 @@ func (rec serviceRecord) toModel() models.ServiceRecord {
|
||||
ChangedEngineAirFilter: rec.ChangedEngineAirFilter,
|
||||
ChangedCabinAirFilter: rec.ChangedCabinAirFilter,
|
||||
Notes: rec.Notes,
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// servicePayload omits the file field — see attachmentOf.
|
||||
func servicePayload(r models.ServiceRecord) map[string]any {
|
||||
return map[string]any{
|
||||
"car": r.Car,
|
||||
@@ -158,6 +161,18 @@ func servicePayload(r models.ServiceRecord) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// --- attachments ---
|
||||
|
||||
// attachmentOf renders a PocketBase file field into the model's attachment pair.
|
||||
//
|
||||
// It has no counterpart on the write side on purpose: attachments move over
|
||||
// multipart via their own endpoint (attachments.go), never as JSON, so a
|
||||
// metadata write must not carry a file field that would blank an existing
|
||||
// upload.
|
||||
func attachmentOf(file string) models.Attachment {
|
||||
return models.Attachment{FileName: file, HasFile: file != ""}
|
||||
}
|
||||
|
||||
// --- parts ---
|
||||
|
||||
type partRecord struct {
|
||||
@@ -166,6 +181,8 @@ type partRecord struct {
|
||||
Name string `json:"name"`
|
||||
PartNumber string `json:"part_number"`
|
||||
Category string `json:"category"`
|
||||
Notes string `json:"notes"`
|
||||
File string `json:"file"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
@@ -177,17 +194,21 @@ func (rec partRecord) toModel() models.Part {
|
||||
Name: rec.Name,
|
||||
PartNumber: rec.PartNumber,
|
||||
Category: rec.Category,
|
||||
Notes: rec.Notes,
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// partPayload omits the file field — see attachmentOf.
|
||||
func partPayload(p models.Part) map[string]any {
|
||||
return map[string]any{
|
||||
"car": p.Car,
|
||||
"name": p.Name,
|
||||
"part_number": p.PartNumber,
|
||||
"category": p.Category,
|
||||
"notes": p.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +245,7 @@ type fuelRecord struct {
|
||||
MissedFill bool `json:"missed_fill"`
|
||||
Station string `json:"station"`
|
||||
Notes string `json:"notes"`
|
||||
File string `json:"file"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
@@ -240,11 +262,13 @@ func (rec fuelRecord) toModel() models.FuelEntry {
|
||||
MissedFill: rec.MissedFill,
|
||||
Station: rec.Station,
|
||||
Notes: rec.Notes,
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// fuelPayload omits the file field — see attachmentOf.
|
||||
func fuelPayload(f models.FuelEntry) map[string]any {
|
||||
return map[string]any{
|
||||
"car": f.Car,
|
||||
@@ -277,6 +301,7 @@ type maintenanceRecord struct {
|
||||
InvoiceNumber string `json:"invoice_number"`
|
||||
WarrantyUntil string `json:"warranty_until"`
|
||||
Notes string `json:"notes"`
|
||||
File string `json:"file"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
@@ -298,11 +323,13 @@ func (rec maintenanceRecord) toModel() models.MaintenanceEntry {
|
||||
InvoiceNumber: rec.InvoiceNumber,
|
||||
WarrantyUntil: parsePBDatePtr(rec.WarrantyUntil),
|
||||
Notes: rec.Notes,
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// maintenancePayload omits the file field — see attachmentOf.
|
||||
func maintenancePayload(m models.MaintenanceEntry) map[string]any {
|
||||
return map[string]any{
|
||||
"car": m.Car,
|
||||
@@ -352,15 +379,13 @@ func (rec documentRecord) toModel() models.CarDocument {
|
||||
ExpiryDate: parsePBDatePtr(rec.ExpiryDate),
|
||||
Cost: rec.Cost,
|
||||
Notes: rec.Notes,
|
||||
FileName: rec.File,
|
||||
HasFile: rec.File != "",
|
||||
Attachment: attachmentOf(rec.File),
|
||||
Created: rec.Created,
|
||||
Updated: rec.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// documentPayload omits the file field: attachments move over multipart, never
|
||||
// as JSON, so a metadata write must not blank an existing upload.
|
||||
// documentPayload omits the file field — see attachmentOf.
|
||||
func documentPayload(d models.CarDocument) map[string]any {
|
||||
return map[string]any{
|
||||
"car": d.Car,
|
||||
|
||||
Reference in New Issue
Block a user