feat(defects): file a defect via a board-section modal (T5)

POST /api/defects validates the body with arktype (known section, uuid
project, non-empty trimmed verbatim) and resolves the reporter through a dev
seam (getReporter, ADR 0002) until OAuth lands. DefectForm is a DaisyUI modal
opened by a section click — pick a project, describe the problem, submit.
This commit is contained in:
Julien Calixte
2026-05-27 23:53:49 +02:00
parent bd427ab171
commit cb19b2df55
8 changed files with 337 additions and 8 deletions

View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
// File a defect against a board section (C3, F2). Driven by `sectionId`: when a
// section is clicked the modal opens pre-targeted to it; pick a project, type
// what's wrong, submit. Reporter and timestamp are added server-side.
import { sections } from '~/board/definition'
interface Project {
id: string
name: string
}
interface Defect {
id: string
sectionId: string
projectId: string
verbatim: string
}
const props = defineProps<{ sectionId: string | null }>()
const emit = defineEmits<{ close: []; filed: [Defect] }>()
const project = ref<Project | null>(null)
const verbatim = ref('')
const busy = ref(false)
const error = ref('')
const sectionLabel = computed(
() => sections.find((s) => s.id === props.sectionId)?.label ?? '',
)
const canSubmit = computed(
() => project.value !== null && verbatim.value.trim().length > 0,
)
// Each time a (new) section opens, start from a clean form.
watch(
() => props.sectionId,
() => {
project.value = null
verbatim.value = ''
error.value = ''
},
)
async function submit() {
if (!canSubmit.value || busy.value || !props.sectionId) return
busy.value = true
error.value = ''
try {
const defect = await $fetch<Defect>('/api/defects', {
method: 'POST',
body: {
sectionId: props.sectionId,
projectId: project.value!.id,
verbatim: verbatim.value.trim(),
},
})
emit('filed', defect)
} catch {
error.value = 'Could not file the defect. Please try again.'
} finally {
busy.value = false
}
}
</script>
<template>
<dialog class="modal" :class="{ 'modal-open': sectionId !== null }">
<div class="modal-box">
<h3 class="text-lg font-bold">Report a problem {{ sectionLabel }}</h3>
<form class="mt-4 flex flex-col gap-4" @submit.prevent="submit">
<fieldset class="fieldset">
<legend class="fieldset-legend">Project</legend>
<ProjectAutocomplete v-model="project" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">What's wrong?</legend>
<textarea
v-model="verbatim"
class="textarea textarea-bordered w-full"
rows="3"
placeholder="Describe the problem in your own words…"
data-test="verbatim"
/>
</fieldset>
<p v-if="error" class="text-error text-sm" role="alert">{{ error }}</p>
<div class="modal-action">
<button type="button" class="btn btn-ghost" @click="emit('close')">
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
:disabled="!canSubmit || busy"
data-test="submit"
>
{{ busy ? 'Filing' : 'File defect' }}
</button>
</div>
</form>
</div>
<!-- Click the backdrop to dismiss. -->
<button
type="button"
class="modal-backdrop"
aria-label="Close"
@click="emit('close')"
/>
</dialog>
</template>

View File

@@ -1,13 +1,33 @@
<script setup lang="ts">
const selected = ref<string | null>(null)
// Andon reporting view (F2): the board is the entry point — clicking a section
// opens the defect form for it.
const selectedSectionId = ref<string | null>(null)
const confirmation = ref<string | null>(null)
function onFiled() {
selectedSectionId.value = null
confirmation.value = 'Thanks — your report was filed.'
setTimeout(() => (confirmation.value = null), 4000)
}
</script>
<template>
<main>
<h1>Andon report a board problem</h1>
<BoardView @select="(id) => (selected = id)" />
<p v-if="selected">
Selected section: <code>{{ selected }}</code> the defect form arrives in Task 5.
</p>
<main class="mx-auto flex max-w-5xl flex-col items-center gap-6 p-6">
<header class="text-center">
<h1>Andon report a board problem</h1>
<p class="opacity-70">Click a section to report a problem.</p>
</header>
<BoardView @select="(id) => (selectedSectionId = id)" />
<DefectForm
:section-id="selectedSectionId"
@close="selectedSectionId = null"
@filed="onFiled"
/>
<div v-if="confirmation" class="toast toast-end">
<div class="alert alert-success">{{ confirmation }}</div>
</div>
</main>
</template>