task status implementation

This commit is contained in:
2026-02-17 17:00:00 -06:00
parent 3d6e29d2c0
commit b8f5db030c
5 changed files with 196 additions and 39 deletions
+67
View File
@@ -0,0 +1,67 @@
import { type Component } from "solid-js";
import { CheckCircle2 } from "lucide-solid";
import { cn } from "@/lib/utils";
interface StatusCircleProps {
status: number;
size?: number;
className?: string;
}
export const StatusCircle: Component<StatusCircleProps> = (props) => {
const size = () => props.size || 24;
const radius = () => (size() / 2) - 2; // Subtract stroke width/padding
const circumference = () => 2 * Math.PI * radius();
// Status 0: Empty Circle
// Status 10: CheckCircle2 (Green)
// Status 1-9: Progress Ring
const strokeDashoffset = () => {
// Invert to fill clockwise?
// Progress 1-9.
// 1 = 10%, 9 = 90%
const percentage = Math.max(0, Math.min(10, props.status)) / 10;
return circumference() * (1 - percentage);
};
return (
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
{props.status >= 10 ? (
<CheckCircle2 size={size()} class="text-green-500" />
) : (
// Progress Circle
<div class="relative group">
{/* Background Circle (Ghost) */}
<svg width={size()} height={size()} class="rotate-[-90deg]">
<circle
cx={size() / 2}
cy={size() / 2}
r={radius()}
fill="transparent"
stroke="currentColor"
stroke-width="2"
class="text-muted-foreground/20"
/>
{/* Progress Arc */}
<circle
cx={size() / 2}
cy={size() / 2}
r={radius()}
fill="transparent"
stroke="currentColor"
stroke-width="2"
stroke-dasharray={circumference().toString()}
stroke-dashoffset={strokeDashoffset()}
stroke-linecap="round"
class="text-primary transition-all duration-300"
/>
</svg>
{/* Hover indicator (maybe show check on hover?)
User didn't ask for generic hover, just visual update.
*/}
</div>
)}
</div>
);
};
+36 -14
View File
@@ -1,11 +1,13 @@
import { type Component, createMemo } from "solid-js"; import { type Component, createMemo, For } from "solid-js";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store"; import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask } from "@/store";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid"; import { Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { For } from "solid-js";
import { useTheme } from "./ThemeProvider"; import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors"; import { getThemeAdjustedColor } from "@/lib/colors";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { STATUS_OPTIONS } from "@/lib/constants";
import { StatusCircle } from "./StatusCircle";
export const TaskCard: Component<{ task: Task }> = (props) => { export const TaskCard: Component<{ task: Task }> = (props) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
@@ -32,16 +34,36 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
)} )}
onClick={() => setActiveTaskId(props.task.id)} onClick={() => setActiveTaskId(props.task.id)}
> >
<button {/* Status Dropdown */}
onClick={(e) => { e.stopPropagation(); toggleTask(props.task.id); }} <div class="mr-2 sm:mr-3 shrink-0" onClick={(e) => e.stopPropagation()}>
class="mr-3 sm:mr-4 text-muted-foreground hover:text-primary transition-colors shrink-0" <Select<any>
> value={(props.task.status ?? 0).toString()}
{props.task.completed ? ( onChange={(val: any) => {
<CheckCircle2 class="text-green-500" size={22} /> const value = typeof val === 'object' ? val?.value : val;
) : ( if (value) {
<Circle size={22} /> const s = parseInt(value);
)} if (!isNaN(s)) updateTask(props.task.id, { status: s });
</button> }
}}
options={STATUS_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Status"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
onDoubleClick={(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
updateTask(props.task.id, { status: 10 });
}}
>
<StatusCircle status={props.task.status ?? 0} size={26} />
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex-1 min-w-0 flex flex-col"> <div class="flex-1 min-w-0 flex flex-col">
<h3 class={cn( <h3 class={cn(
+58 -21
View File
@@ -4,11 +4,12 @@ import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, sha
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid"; import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
import { StatusCircle } from "./StatusCircle";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store"; import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker"; import { TagPicker } from "./TagPicker";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants"; import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS, STATUS_OPTIONS } from "@/lib/constants";
import { store } from "@/store"; import { store } from "@/store";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toast } from "solid-sonner"; import { toast } from "solid-sonner";
@@ -72,6 +73,14 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
} }
}; };
const updateStatus = (val: any) => {
const value = typeof val === 'object' ? val.value : val;
const s = parseInt(value);
if (!isNaN(s)) {
updateTask(props.task.id, { status: s });
}
};
const updateUrgency = (val: any) => { const updateUrgency = (val: any) => {
const value = typeof val === 'object' ? val.value : val; const value = typeof val === 'object' ? val.value : val;
const u = parseInt(value); const u = parseInt(value);
@@ -119,29 +128,57 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
> >
{/* Header Area */} {/* Header Area */}
<div class="px-6 pt-6 pb-2 shrink-0"> <div class="px-6 pt-6 pb-2 shrink-0">
<textarea <div class="flex items-start gap-4">
id="task-detail-title" <div class="shrink-0 mt-0.5">
name="task-title" <Select<any>
value={title()} value={(props.task.status ?? 0).toString()}
onInput={(e) => { onChange={(val: string | null) => val && updateStatus(val)}
updateTitle(e.currentTarget.value); options={STATUS_OPTIONS}
// Auto-resize optionValue="value"
e.currentTarget.style.height = 'auto'; optionTextValue="label"
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px'; placeholder="Status"
}} itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
ref={(el) => { >
setTimeout(() => { <SelectTrigger
el.style.height = 'auto'; class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 px-0 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
el.style.height = el.scrollHeight + 'px'; onDoubleClick={(e: MouseEvent) => {
}, 0); e.preventDefault();
}} e.stopPropagation();
rows={1} updateStatus("10");
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight" }}
placeholder="Untitled Task" >
/> <StatusCircle status={props.task.status ?? 0} size={28} />
</SelectTrigger>
<SelectContent />
</Select>
</div>
<textarea
id="task-detail-title"
name="task-title"
value={title()}
onInput={(e) => {
updateTitle(e.currentTarget.value);
// Auto-resize
e.currentTarget.style.height = 'auto';
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
}}
ref={(el) => {
setTimeout(() => {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}, 0);
}}
rows={1}
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight flex-1 min-w-0"
placeholder="Untitled Task"
/>
</div>
{/* Properties Bar */} {/* Properties Bar */}
<div class="flex flex-wrap items-center gap-2 sm:gap-4 mt-2 text-sm"> <div class="flex flex-wrap items-center gap-2 sm:gap-4 mt-2 text-sm">
{/* Priority */} {/* Priority */}
<div class="flex items-center gap-1 group shrink-0"> <div class="flex items-center gap-1 group shrink-0">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Priority</span> <span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Priority</span>
+14
View File
@@ -54,3 +54,17 @@ export const SIZE_OPTIONS = [
{ value: "1", label: "1 - Five minutes" }, { value: "1", label: "1 - Five minutes" },
{ value: "0", label: "0 - Less than a minute" } { value: "0", label: "0 - Less than a minute" }
]; ];
export const STATUS_OPTIONS = [
{ value: "10", label: "10 - Complete" },
{ value: "9", label: "9" },
{ value: "8", label: "8" },
{ value: "7", label: "7" },
{ value: "6", label: "6" },
{ value: "5", label: "5" },
{ value: "4", label: "4" },
{ value: "3", label: "3" },
{ value: "2", label: "2" },
{ value: "1", label: "1" },
{ value: "0", label: "0 - Not Yet Started" }
];
+21 -4
View File
@@ -23,7 +23,8 @@ export interface Task {
startDate: string; startDate: string;
dueDate: string; dueDate: string;
priority: number; // 1-10 priority: number; // 1-10
completed: boolean; status: number; // 0-10
completed: boolean; // Deprecated, kept for compat/computed from status = 10
tags: string[]; tags: string[];
ownerId: string | null; // Add ownerId to interface ownerId: string | null; // Add ownerId to interface
content?: string; // HTML content from Tiptap content?: string; // HTML content from Tiptap
@@ -46,7 +47,7 @@ export const checkRecurringTasks = () => {
const todayStr = nowObj.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time const todayStr = nowObj.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
tasks.forEach(task => { tasks.forEach(task => {
if (!task.completed || !task.recurrence) return; if (task.status !== 10 || !task.recurrence) return;
// If deleted, we don't recurse? // If deleted, we don't recurse?
if (task.deletedAt) return; if (task.deletedAt) return;
@@ -101,6 +102,7 @@ export const checkRecurringTasks = () => {
if (shouldReset) { if (shouldReset) {
console.log(`Resetting recurring task: ${task.title}`); console.log(`Resetting recurring task: ${task.title}`);
updateTask(task.id, { updateTask(task.id, {
status: 0,
completed: false, completed: false,
recurrence: { recurrence: {
...task.recurrence, ...task.recurrence,
@@ -370,6 +372,13 @@ export const getCombinedScore = (task: Task): number => {
const sizeScore = ((task.size ?? 5) - 5) * -0.4; const sizeScore = ((task.size ?? 5) - 5) * -0.4;
baseScore += sizeScore; baseScore += sizeScore;
// Status adjustment: 0-9 -> 0.0-2.0 weight
if (task.status < 10) {
// Map 0-9 to 0-2 range
const statusWeight = (task.status / 9) * 2.0;
baseScore += statusWeight;
}
return baseScore; return baseScore;
}; };
@@ -387,7 +396,8 @@ const mapRecordToTask = (r: any): Task => {
startDate: r.startDate, startDate: r.startDate,
dueDate: r.dueDate, dueDate: r.dueDate,
priority: r.priority, priority: r.priority,
completed: r.completed, status: r.status ?? (r.completed ? 10 : 0),
completed: r.status === 10 || r.completed,
tags: tags, tags: tags,
ownerId: r.user || null, ownerId: r.user || null,
content: r.content, content: r.content,
@@ -1013,6 +1023,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
startDate, startDate,
dueDate, dueDate,
priority, priority,
status: 0,
completed: false, completed: false,
tags, tags,
ownerId: pb.authStore.model?.id || "", ownerId: pb.authStore.model?.id || "",
@@ -1032,6 +1043,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
startDate, startDate,
dueDate, dueDate,
priority, priority,
status: 0,
completed: false, completed: false,
tags, tags,
content, content,
@@ -1225,7 +1237,11 @@ export const setMatrixScaleDays = async (days: number) => {
export const toggleTask = (id: string) => { export const toggleTask = (id: string) => {
const task = store.tasks.find(t => t.id === id); const task = store.tasks.find(t => t.id === id);
if (task) { if (task) {
updateTask(id, { completed: !task.completed }); const newCompleted = !task.completed;
updateTask(id, {
completed: newCompleted,
status: newCompleted ? 10 : 0
});
} }
}; };
@@ -1672,6 +1688,7 @@ export const splitTask = async (taskId: string, userId: string) => {
tags: task.tags, tags: task.tags,
startDate: task.startDate, startDate: task.startDate,
dueDate: task.dueDate, dueDate: task.dueDate,
status: task.status,
completed: task.completed, completed: task.completed,
deletedAt: null, deletedAt: null,
sharedWith: [], // Start fresh with no shares sharedWith: [], // Start fresh with no shares