|
10 | 10 | removeProject, |
11 | 11 | addMember, |
12 | 12 | removeMember, |
| 13 | + transferLead, |
13 | 14 | } from "$lib/data/private/projects.remote"; |
14 | 15 | import { useToast } from "$lib/components/toast/controls.svelte"; |
15 | | - import { Folder, Plus, X } from "lucide-svelte"; |
| 16 | + import { Folder, Plus, X, ArrowRightLeft } from "lucide-svelte"; |
16 | 17 | import type { ProjectCategory } from "$lib/shared/models/schema"; |
17 | 18 |
|
18 | 19 | const toast = useToast(); |
|
23 | 24 | let showAddMember = $state(false); |
24 | 25 | let newMemberId = $state<string | null>(null); |
25 | 26 | let newMemberRole = $state<"member" | "lead">("member"); |
| 27 | + let showTransferLead = $state(false); |
| 28 | + let transferTargetId = $state<string | null>(null); |
| 29 | +
|
| 30 | + const currentLead = $derived(project?.projectMembers.find((pm) => pm.role === "lead")); |
| 31 | + const otherMembers = $derived(project?.projectMembers.filter((pm) => pm.role !== "lead") ?? []); |
| 32 | + const canTransferLead = $derived((otherMembers?.length ?? 0) > 0); |
26 | 33 |
|
27 | 34 | async function handleSubmit(data: { |
28 | 35 | slug: string; |
|
120 | 127 | } |
121 | 128 | } |
122 | 129 | } |
| 130 | +
|
| 131 | + async function handleTransferLead() { |
| 132 | + if (!transferTargetId || !currentLead) return; |
| 133 | +
|
| 134 | + const targetMember = otherMembers.find((m) => m.memberId === transferTargetId); |
| 135 | + if (!targetMember) return; |
| 136 | +
|
| 137 | + const confirmed = await confirm({ |
| 138 | + title: "Transfer lead role?", |
| 139 | + description: `Transfer lead role from ${currentLead.member.name} to ${targetMember.member.name}?`, |
| 140 | + confirmText: "Transfer", |
| 141 | + variant: "warning", |
| 142 | + }); |
| 143 | +
|
| 144 | + if (confirmed) { |
| 145 | + try { |
| 146 | + await transferLead({ projectId: id, newLeadMemberId: transferTargetId }).updates( |
| 147 | + getProject(id), |
| 148 | + ); |
| 149 | + toast.show("Lead transferred", "success"); |
| 150 | + showTransferLead = false; |
| 151 | + transferTargetId = null; |
| 152 | + } catch (error) { |
| 153 | + toast.show(error instanceof Error ? error.message : "Failed to transfer"); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
123 | 157 | </script> |
124 | 158 |
|
125 | 159 | <svelte:head> |
|
152 | 186 | <!-- Team Members Bar --> |
153 | 187 | <div class="flex items-center gap-3 border-b border-zinc-200 bg-zinc-50 px-4 py-2"> |
154 | 188 | <span class="text-xs font-medium text-zinc-500">Team:</span> |
155 | | - <div class="flex items-center gap-1"> |
| 189 | + <div class="flex flex-1 items-center gap-1"> |
156 | 190 | {#each project.projectMembers as pm (pm.memberId)} |
157 | 191 | <div |
158 | 192 | class="group flex items-center gap-1.5 rounded-full bg-white py-1 pr-2 pl-1 text-xs shadow-sm ring-1 ring-zinc-200" |
|
192 | 226 | <Plus class="h-4 w-4" /> |
193 | 227 | </button> |
194 | 228 | </div> |
| 229 | + {#if canTransferLead} |
| 230 | + <button |
| 231 | + type="button" |
| 232 | + onclick={() => (showTransferLead = true)} |
| 233 | + class="flex items-center gap-1.5 rounded-lg bg-white px-2.5 py-1.5 text-xs font-medium text-zinc-700 shadow-sm ring-1 ring-zinc-200 transition-colors hover:bg-zinc-50" |
| 234 | + > |
| 235 | + <ArrowRightLeft class="h-3.5 w-3.5" /> |
| 236 | + Transfer Lead |
| 237 | + </button> |
| 238 | + {/if} |
195 | 239 | </div> |
196 | 240 |
|
197 | 241 | <!-- Main Form --> |
|
268 | 312 | </div> |
269 | 313 | </div> |
270 | 314 | {/if} |
| 315 | + |
| 316 | + <!-- Transfer Lead Modal --> |
| 317 | + {#if showTransferLead} |
| 318 | + <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> |
| 319 | + <div class="w-full max-w-sm rounded-xl bg-white p-5 shadow-xl"> |
| 320 | + <h3 class="font-semibold text-zinc-900">Transfer lead role</h3> |
| 321 | + <div class="mt-4 space-y-1.5"> |
| 322 | + <label for="transferTarget" class="text-sm font-medium text-zinc-700" |
| 323 | + >New lead member</label |
| 324 | + > |
| 325 | + <select |
| 326 | + id="transferTarget" |
| 327 | + bind:value={transferTargetId} |
| 328 | + class="w-full rounded-lg border border-zinc-200 px-3 py-2 text-sm text-zinc-900" |
| 329 | + > |
| 330 | + <option value={null}>Select</option> |
| 331 | + {#each otherMembers as pm (pm.memberId)} |
| 332 | + <option value={pm.memberId}>{pm.member.name}</option> |
| 333 | + {/each} |
| 334 | + </select> |
| 335 | + </div> |
| 336 | + <div class="mt-5 flex justify-end gap-2"> |
| 337 | + <button |
| 338 | + type="button" |
| 339 | + onclick={() => { |
| 340 | + showTransferLead = false; |
| 341 | + transferTargetId = null; |
| 342 | + }} |
| 343 | + class="rounded-lg px-3 py-2 text-sm font-medium text-zinc-600 hover:bg-zinc-100" |
| 344 | + > |
| 345 | + Cancel |
| 346 | + </button> |
| 347 | + <button |
| 348 | + type="button" |
| 349 | + onclick={handleTransferLead} |
| 350 | + disabled={!transferTargetId} |
| 351 | + class="rounded-lg bg-zinc-900 px-3 py-2 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50" |
| 352 | + > |
| 353 | + Transfer |
| 354 | + </button> |
| 355 | + </div> |
| 356 | + </div> |
| 357 | + </div> |
| 358 | + {/if} |
271 | 359 | {/if} |
272 | 360 | </svelte:boundary> |
0 commit comments