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
@@ -2,6 +2,8 @@
|
||||
import { ref } from "vue";
|
||||
import { api } from "../api";
|
||||
import { formatKm } from "../lib/format.js";
|
||||
import { applyAttachment } from "../lib/attachment.js";
|
||||
import AttachmentField from "./AttachmentField.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -23,6 +25,9 @@ const form = ref({
|
||||
notes: props.service?.notes ?? "",
|
||||
});
|
||||
|
||||
const file = ref(null);
|
||||
const removeFile = ref(false);
|
||||
|
||||
function toDateInput(value) {
|
||||
const d = new Date(value);
|
||||
return isNaN(d) ? "" : d.toISOString().slice(0, 10);
|
||||
@@ -44,7 +49,10 @@ async function submit() {
|
||||
const saved = isEdit
|
||||
? await api.updateService(props.service.id, payload)
|
||||
: await api.createService(payload);
|
||||
emit("saved", saved);
|
||||
emit("saved", await applyAttachment(api.files.services, saved, {
|
||||
file: file.value,
|
||||
remove: removeFile.value,
|
||||
}));
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
} finally {
|
||||
@@ -69,10 +77,16 @@ async function submit() {
|
||||
</div>
|
||||
<fieldset class="rounded-control border border-subtle p-3">
|
||||
<legend class="eyebrow px-1">Changed parts</legend>
|
||||
<label class="flex items-center gap-2 py-1 text-sm text-body"><input type="checkbox" v-model="form.changedOil" class="accent-[var(--accent)]" /> Oil & oil filter</label>
|
||||
<label class="flex items-center gap-2 py-1 text-sm text-body"><input type="checkbox" v-model="form.changedOil" class="accent-[var(--accent)]" /> Oil & Oil filter</label>
|
||||
<label class="flex items-center gap-2 py-1 text-sm text-body"><input type="checkbox" v-model="form.changedEngineAirFilter" class="accent-[var(--accent)]" /> Engine air filter</label>
|
||||
<label class="flex items-center gap-2 py-1 text-sm text-body"><input type="checkbox" v-model="form.changedCabinAirFilter" class="accent-[var(--accent)]" /> Cabin air filter</label>
|
||||
</fieldset>
|
||||
<AttachmentField
|
||||
v-model:file="file"
|
||||
v-model:remove="removeFile"
|
||||
:record="service"
|
||||
legend="Receipt or service-book page"
|
||||
/>
|
||||
<div>
|
||||
<label class="dh-label">Notes</label>
|
||||
<input v-model="form.notes" class="dh-input" />
|
||||
|
||||
Reference in New Issue
Block a user