Mega push vol 3
This commit is contained in:
@@ -13,6 +13,26 @@
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
card_id: string;
|
||||
user_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
profiles?: { full_name: string | null; email: string };
|
||||
}
|
||||
|
||||
interface Member {
|
||||
id: string;
|
||||
user_id: string;
|
||||
profiles: {
|
||||
id: string;
|
||||
full_name: string | null;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
card: KanbanCard | null;
|
||||
isOpen: boolean;
|
||||
@@ -23,6 +43,7 @@
|
||||
columnId?: string;
|
||||
userId?: string;
|
||||
onCreate?: (card: KanbanCard) => void;
|
||||
members?: Member[];
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -35,6 +56,7 @@
|
||||
columnId,
|
||||
userId,
|
||||
onCreate,
|
||||
members = [],
|
||||
}: Props = $props();
|
||||
|
||||
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
||||
@@ -42,20 +64,38 @@
|
||||
let title = $state("");
|
||||
let description = $state("");
|
||||
let checklist = $state<ChecklistItem[]>([]);
|
||||
let comments = $state<Comment[]>([]);
|
||||
let newItemTitle = $state("");
|
||||
let newComment = $state("");
|
||||
let assigneeId = $state<string | null>(null);
|
||||
let dueDate = $state<string>("");
|
||||
let priority = $state<string>("medium");
|
||||
let isLoading = $state(false);
|
||||
let isSaving = $state(false);
|
||||
let showAssigneePicker = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
if (mode === "edit" && card) {
|
||||
title = card.title;
|
||||
description = card.description ?? "";
|
||||
assigneeId = (card as any).assignee_id ?? null;
|
||||
dueDate = (card as any).due_date
|
||||
? new Date((card as any).due_date)
|
||||
.toISOString()
|
||||
.split("T")[0]
|
||||
: "";
|
||||
priority = (card as any).priority ?? "medium";
|
||||
loadChecklist();
|
||||
loadComments();
|
||||
} else if (mode === "create") {
|
||||
title = "";
|
||||
description = "";
|
||||
assigneeId = null;
|
||||
dueDate = "";
|
||||
priority = "medium";
|
||||
checklist = [];
|
||||
comments = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -65,7 +105,7 @@
|
||||
isLoading = true;
|
||||
|
||||
const { data } = await supabase
|
||||
.from("checklist_items")
|
||||
.from("kanban_checklist_items")
|
||||
.select("*")
|
||||
.eq("card_id", card.id)
|
||||
.order("position");
|
||||
@@ -74,6 +114,27 @@
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
if (!card) return;
|
||||
|
||||
const { data } = await supabase
|
||||
.from("kanban_comments")
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
card_id,
|
||||
user_id,
|
||||
content,
|
||||
created_at,
|
||||
profiles:user_id (full_name, email)
|
||||
`,
|
||||
)
|
||||
.eq("card_id", card.id)
|
||||
.order("created_at", { ascending: true });
|
||||
|
||||
comments = (data ?? []) as Comment[];
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (mode === "create") {
|
||||
await handleCreate();
|
||||
@@ -88,11 +149,21 @@
|
||||
.update({
|
||||
title,
|
||||
description: description || null,
|
||||
assignee_id: assigneeId,
|
||||
due_date: dueDate || null,
|
||||
priority,
|
||||
})
|
||||
.eq("id", card.id);
|
||||
|
||||
if (!error) {
|
||||
onUpdate({ ...card, title, description: description || null });
|
||||
onUpdate({
|
||||
...card,
|
||||
title,
|
||||
description: description || null,
|
||||
assignee_id: assigneeId,
|
||||
due_date: dueDate || null,
|
||||
priority,
|
||||
} as KanbanCard);
|
||||
}
|
||||
isSaving = false;
|
||||
}
|
||||
@@ -133,7 +204,7 @@
|
||||
|
||||
const position = checklist.length;
|
||||
const { data, error } = await supabase
|
||||
.from("checklist_items")
|
||||
.from("kanban_checklist_items")
|
||||
.insert({
|
||||
card_id: card.id,
|
||||
title: newItemTitle,
|
||||
@@ -149,9 +220,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddComment() {
|
||||
if (!card || !newComment.trim() || !userId) return;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("kanban_comments")
|
||||
.insert({
|
||||
card_id: card.id,
|
||||
user_id: userId,
|
||||
content: newComment,
|
||||
})
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
card_id,
|
||||
user_id,
|
||||
content,
|
||||
created_at,
|
||||
profiles:user_id (full_name, email)
|
||||
`,
|
||||
)
|
||||
.single();
|
||||
|
||||
if (!error && data) {
|
||||
comments = [...comments, data as Comment];
|
||||
newComment = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleItem(item: ChecklistItem) {
|
||||
const { error } = await supabase
|
||||
.from("checklist_items")
|
||||
.from("kanban_checklist_items")
|
||||
.update({ completed: !item.completed })
|
||||
.eq("id", item.id);
|
||||
|
||||
@@ -164,7 +263,7 @@
|
||||
|
||||
async function deleteItem(itemId: string) {
|
||||
const { error } = await supabase
|
||||
.from("checklist_items")
|
||||
.from("kanban_checklist_items")
|
||||
.delete()
|
||||
.eq("id", itemId);
|
||||
|
||||
@@ -173,6 +272,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function getAssignee(id: string | null) {
|
||||
if (!id) return null;
|
||||
return members.find((m) => m.user_id === id);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!card || !confirm("Delete this card?")) return;
|
||||
|
||||
@@ -207,6 +320,129 @@
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<!-- Assignee, Due Date, Priority Row -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<!-- Assignee -->
|
||||
<div class="relative">
|
||||
<label class="block text-sm font-medium text-light mb-1"
|
||||
>Assignee</label
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-left text-sm flex items-center gap-2 hover:border-light/40 transition-colors"
|
||||
onclick={() =>
|
||||
(showAssigneePicker = !showAssigneePicker)}
|
||||
>
|
||||
{#if assigneeId && getAssignee(assigneeId)}
|
||||
{@const assignee = getAssignee(assigneeId)}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs text-primary"
|
||||
>
|
||||
{(assignee?.profiles.full_name ||
|
||||
assignee?.profiles.email ||
|
||||
"?")[0].toUpperCase()}
|
||||
</div>
|
||||
<span class="text-light truncate"
|
||||
>{assignee?.profiles.full_name ||
|
||||
assignee?.profiles.email}</span
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-light/10 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-light/40"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-light/40">Unassigned</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if showAssigneePicker}
|
||||
<div
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-dark border border-light/20 rounded-lg shadow-lg z-10 max-h-48 overflow-y-auto"
|
||||
>
|
||||
<button
|
||||
class="w-full px-3 py-2 text-left text-sm text-light/60 hover:bg-light/5 flex items-center gap-2"
|
||||
onclick={() => {
|
||||
assigneeId = null;
|
||||
showAssigneePicker = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-light/10"
|
||||
></div>
|
||||
Unassigned
|
||||
</button>
|
||||
{#each members as member}
|
||||
<button
|
||||
class="w-full px-3 py-2 text-left text-sm hover:bg-light/5 flex items-center gap-2 {assigneeId ===
|
||||
member.user_id
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-light'}"
|
||||
onclick={() => {
|
||||
assigneeId = member.user_id;
|
||||
showAssigneePicker = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs"
|
||||
>
|
||||
{(member.profiles.full_name ||
|
||||
member.profiles.email ||
|
||||
"?")[0].toUpperCase()}
|
||||
</div>
|
||||
{member.profiles.full_name ||
|
||||
member.profiles.email}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div>
|
||||
<label
|
||||
for="due-date"
|
||||
class="block text-sm font-medium text-light mb-1"
|
||||
>Due Date</label
|
||||
>
|
||||
<input
|
||||
id="due-date"
|
||||
type="date"
|
||||
bind:value={dueDate}
|
||||
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Priority -->
|
||||
<div>
|
||||
<label
|
||||
for="priority"
|
||||
class="block text-sm font-medium text-light mb-1"
|
||||
>Priority</label
|
||||
>
|
||||
<select
|
||||
id="priority"
|
||||
bind:value={priority}
|
||||
class="w-full px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm font-medium text-light"
|
||||
@@ -302,6 +538,71 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
{#if mode === "edit"}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-light mb-3"
|
||||
>Comments</label
|
||||
>
|
||||
<div class="space-y-3 mb-3 max-h-48 overflow-y-auto">
|
||||
{#each comments as comment}
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-primary/20 flex-shrink-0 flex items-center justify-center text-xs text-primary"
|
||||
>
|
||||
{((comment.profiles as any)?.full_name ||
|
||||
(comment.profiles as any)?.email ||
|
||||
"?")[0].toUpperCase()}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium text-light"
|
||||
>
|
||||
{(comment.profiles as any)
|
||||
?.full_name ||
|
||||
(comment.profiles as any)
|
||||
?.email ||
|
||||
"Unknown"}
|
||||
</span>
|
||||
<span class="text-xs text-light/40"
|
||||
>{formatDate(
|
||||
comment.created_at,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-light/70 mt-0.5">
|
||||
{comment.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if comments.length === 0}
|
||||
<p class="text-sm text-light/40 text-center py-2">
|
||||
No comments yet
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 px-3 py-2 bg-dark border border-light/20 rounded-lg text-sm text-light placeholder:text-light/40 focus:outline-none focus:border-primary"
|
||||
placeholder="Add a comment..."
|
||||
bind:value={newComment}
|
||||
onkeydown={(e) =>
|
||||
e.key === "Enter" && handleAddComment()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={handleAddComment}
|
||||
disabled={!newComment.trim()}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between pt-3 border-t border-light/10"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user