task status implementation
This commit is contained in:
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user