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>
79 lines
2.5 KiB
Vue
79 lines
2.5 KiB
Vue
<script setup>
|
||
import { ref } from "vue";
|
||
import { api } from "../api";
|
||
import { applyAttachment } from "../lib/attachment.js";
|
||
import AttachmentField from "./AttachmentField.vue";
|
||
import Modal from "./Modal.vue";
|
||
|
||
const props = defineProps({
|
||
carId: { type: String, required: true },
|
||
part: { type: Object, default: null },
|
||
});
|
||
const emit = defineEmits(["saved", "close"]);
|
||
|
||
const isEdit = !!props.part;
|
||
const saving = ref(false);
|
||
const error = ref("");
|
||
const form = ref({
|
||
name: props.part?.name ?? "",
|
||
partNumber: props.part?.partNumber ?? "",
|
||
category: props.part?.category ?? "",
|
||
notes: props.part?.notes ?? "",
|
||
});
|
||
|
||
const file = ref(null);
|
||
const removeFile = ref(false);
|
||
|
||
async function submit() {
|
||
saving.value = true;
|
||
error.value = "";
|
||
try {
|
||
const payload = {
|
||
car: props.carId,
|
||
name: form.value.name.trim(),
|
||
partNumber: form.value.partNumber.trim(),
|
||
category: form.value.category.trim(),
|
||
notes: form.value.notes.trim(),
|
||
};
|
||
const saved = isEdit
|
||
? await api.updatePart(props.part.id, payload)
|
||
: await api.createPart(payload);
|
||
emit("saved", await applyAttachment(api.files.parts, saved, {
|
||
file: file.value,
|
||
remove: removeFile.value,
|
||
}));
|
||
} catch (e) {
|
||
error.value = e.message;
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Modal :title="isEdit ? 'Edit part' : 'Add part'" @close="emit('close')">
|
||
<p v-if="error" class="mb-3 rounded-control bg-danger-soft px-3 py-2 text-sm font-medium text-danger">{{ error }}</p>
|
||
<form class="space-y-3" @submit.prevent="submit">
|
||
<div>
|
||
<label class="dh-label">Part name *</label>
|
||
<input v-model="form.name" required placeholder="Oil Filter" class="dh-input" />
|
||
</div>
|
||
<div>
|
||
<label class="dh-label">Part number</label>
|
||
<input v-model="form.partNumber" placeholder="04152-YZZA7" class="dh-input data" />
|
||
</div>
|
||
<div>
|
||
<label class="dh-label">Notes</label>
|
||
<input v-model="form.notes" placeholder="Fits 2015–2020 · buy in pairs" class="dh-input" />
|
||
</div>
|
||
<AttachmentField v-model:file="file" v-model:remove="removeFile" :record="part" legend="Photo or spec sheet" />
|
||
<div class="mt-4 flex justify-end gap-2">
|
||
<button type="button" class="dh-btn dh-btn-ghost" @click="emit('close')">Cancel</button>
|
||
<button type="submit" :disabled="saving" class="dh-btn dh-btn-primary">
|
||
{{ saving ? "Saving…" : isEdit ? "Save changes" : "Add part" }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
</template>
|