feat: task management enhancements (cancelled status, recurrence, timestamps)
- Add 'cancelled' status to TaskStatus type, StatusBadge, TaskCard, TaskEditorView, TaskViewerView, TasksListView - Add RecurrenceEditor component (none / interval / calendar rules) - TaskEditorView: wire RecurrenceEditor, show started_at/completed_at timestamps read-only, include recurrence_rule in save payload - TaskViewerView: show recurrence summary, timestamps in meta row - tasks.ts: statusFilter/priorityFilter as arrays, add recurrence_rule to updateTask and createTask payloads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Record<string, unknown> | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: Record<string, unknown> | null): void;
|
||||
}>();
|
||||
|
||||
type RecurrenceType = "none" | "interval" | "calendar";
|
||||
type IntervalUnit = "day" | "week" | "month" | "year";
|
||||
type CalendarUnit = "month" | "year";
|
||||
|
||||
const rType = ref<RecurrenceType>("none");
|
||||
const intervalEvery = ref(1);
|
||||
const intervalUnit = ref<IntervalUnit>("week");
|
||||
const calendarUnit = ref<CalendarUnit>("month");
|
||||
const calendarDay = ref(1);
|
||||
const calendarMonth = ref(1);
|
||||
|
||||
function ruleFromState(): Record<string, unknown> | null {
|
||||
if (rType.value === "none") return null;
|
||||
if (rType.value === "interval") {
|
||||
return { type: "interval", every: intervalEvery.value, unit: intervalUnit.value };
|
||||
}
|
||||
// calendar
|
||||
const rule: Record<string, unknown> = {
|
||||
type: "calendar",
|
||||
unit: calendarUnit.value,
|
||||
day_of_month: calendarDay.value,
|
||||
};
|
||||
if (calendarUnit.value === "year") {
|
||||
rule.month = calendarMonth.value;
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
function loadFromRule(rule: Record<string, unknown> | null) {
|
||||
if (!rule) {
|
||||
rType.value = "none";
|
||||
return;
|
||||
}
|
||||
const t = rule.type as string;
|
||||
if (t === "interval") {
|
||||
rType.value = "interval";
|
||||
intervalEvery.value = (rule.every as number) ?? 1;
|
||||
intervalUnit.value = (rule.unit as IntervalUnit) ?? "week";
|
||||
} else if (t === "calendar") {
|
||||
rType.value = "calendar";
|
||||
calendarUnit.value = (rule.unit as CalendarUnit) ?? "month";
|
||||
calendarDay.value = (rule.day_of_month as number) ?? 1;
|
||||
calendarMonth.value = (rule.month as number) ?? 1;
|
||||
} else {
|
||||
rType.value = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial value
|
||||
loadFromRule(props.modelValue);
|
||||
|
||||
// Watch for external changes (e.g., task loaded)
|
||||
watch(() => props.modelValue, (val) => {
|
||||
loadFromRule(val);
|
||||
}, { deep: true });
|
||||
|
||||
// Emit on any state change
|
||||
function onChange() {
|
||||
emit("update:modelValue", ruleFromState());
|
||||
}
|
||||
|
||||
const monthNames = [
|
||||
"January","February","March","April","May","June",
|
||||
"July","August","September","October","November","December",
|
||||
];
|
||||
|
||||
const calendarDayMax = computed(() =>
|
||||
calendarUnit.value === "month" ? 28 : 28
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rec-editor">
|
||||
<select v-model="rType" class="sb-select" @change="onChange">
|
||||
<option value="none">No recurrence</option>
|
||||
<option value="interval">Every N days/weeks/months</option>
|
||||
<option value="calendar">On a calendar date</option>
|
||||
</select>
|
||||
|
||||
<div v-if="rType === 'interval'" class="rec-row">
|
||||
<span class="rec-label">Every</span>
|
||||
<input
|
||||
v-model.number="intervalEvery"
|
||||
type="number"
|
||||
min="1"
|
||||
max="365"
|
||||
class="rec-num-input"
|
||||
@input="onChange"
|
||||
/>
|
||||
<select v-model="intervalUnit" class="sb-select rec-unit" @change="onChange">
|
||||
<option value="day">day(s)</option>
|
||||
<option value="week">week(s)</option>
|
||||
<option value="month">month(s)</option>
|
||||
<option value="year">year(s)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="rType === 'calendar'" class="rec-row rec-col">
|
||||
<div class="rec-row">
|
||||
<span class="rec-label">Unit</span>
|
||||
<select v-model="calendarUnit" class="sb-select" @change="onChange">
|
||||
<option value="month">Monthly</option>
|
||||
<option value="year">Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="rec-row">
|
||||
<span class="rec-label">Day</span>
|
||||
<input
|
||||
v-model.number="calendarDay"
|
||||
type="number"
|
||||
min="1"
|
||||
:max="calendarDayMax"
|
||||
class="rec-num-input"
|
||||
@input="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="calendarUnit === 'year'" class="rec-row">
|
||||
<span class="rec-label">Month</span>
|
||||
<select v-model.number="calendarMonth" class="sb-select" @change="onChange">
|
||||
<option v-for="(name, i) in monthNames" :key="i + 1" :value="i + 1">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rec-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.rec-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rec-col {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.rec-label {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
min-width: 2.5rem;
|
||||
}
|
||||
.rec-num-input {
|
||||
width: 4rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
border: 1px solid var(--color-input-border, var(--color-border));
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
.rec-num-input:focus { outline: none; border-color: var(--color-primary); }
|
||||
.rec-unit { min-width: 6rem; }
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ const labels: Record<TaskStatus, string> = {
|
||||
todo: "Todo",
|
||||
in_progress: "In Progress",
|
||||
done: "Done",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -48,6 +49,10 @@ const labels: Record<TaskStatus, string> = {
|
||||
background: color-mix(in srgb, var(--color-status-done-bg) 78%, var(--color-status-done) 22%);
|
||||
color: color-mix(in srgb, var(--color-status-done) 85%, #000 15%);
|
||||
}
|
||||
.status-cancelled {
|
||||
background: color-mix(in srgb, var(--color-bg-secondary) 78%, var(--color-text-muted) 22%);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user