Files
root-org/src/lib/components/settings/SettingsRoles.svelte

337 lines
8.4 KiB
Svelte

<script lang="ts">
import { Button, Modal, Input } from "$lib/components/ui";
import { toasts } from "$lib/stores/toast.svelte";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
import * as m from "$lib/paraglide/messages";
interface OrgRole {
id: string;
org_id: string;
name: string;
color: string;
permissions: string[];
is_default: boolean;
is_system: boolean;
position: number;
}
interface Props {
supabase: SupabaseClient<Database>;
orgId: string;
roles: OrgRole[];
}
let { supabase, orgId, roles = $bindable() }: Props = $props();
let showRoleModal = $state(false);
let editingRole = $state<OrgRole | null>(null);
let newRoleName = $state("");
let newRoleColor = $state("#6366f1");
let newRolePermissions = $state<string[]>([]);
let isSavingRole = $state(false);
const permissionGroups = [
{
name: "Documents",
permissions: [
"documents.view",
"documents.create",
"documents.edit",
"documents.delete",
],
},
{
name: "Kanban",
permissions: [
"kanban.view",
"kanban.create",
"kanban.edit",
"kanban.delete",
],
},
{
name: "Calendar",
permissions: [
"calendar.view",
"calendar.create",
"calendar.edit",
"calendar.delete",
],
},
{
name: "Members",
permissions: [
"members.view",
"members.invite",
"members.manage",
"members.remove",
],
},
{
name: "Roles",
permissions: [
"roles.view",
"roles.create",
"roles.edit",
"roles.delete",
],
},
{ name: "Settings", permissions: ["settings.view", "settings.edit"] },
];
const roleColors = [
{ value: "#ef4444", label: "Red" },
{ value: "#f59e0b", label: "Amber" },
{ value: "#10b981", label: "Emerald" },
{ value: "#3b82f6", label: "Blue" },
{ value: "#6366f1", label: "Indigo" },
{ value: "#8b5cf6", label: "Violet" },
{ value: "#ec4899", label: "Pink" },
{ value: "#6b7280", label: "Gray" },
];
function openRoleModal(role?: OrgRole) {
if (role) {
editingRole = role;
newRoleName = role.name;
newRoleColor = role.color;
newRolePermissions = [...role.permissions];
} else {
editingRole = null;
newRoleName = "";
newRoleColor = "#6366f1";
newRolePermissions = [
"documents.view",
"kanban.view",
"calendar.view",
"members.view",
];
}
showRoleModal = true;
}
async function saveRole() {
if (!newRoleName.trim()) return;
isSavingRole = true;
if (editingRole) {
const { error } = await supabase
.from("org_roles")
.update({
name: newRoleName,
color: newRoleColor,
permissions: newRolePermissions,
})
.eq("id", editingRole.id);
if (!error) {
roles = roles.map((r) =>
r.id === editingRole!.id
? {
...r,
name: newRoleName,
color: newRoleColor,
permissions: newRolePermissions,
}
: r,
);
}
} else {
const { data: role, error } = await supabase
.from("org_roles")
.insert({
org_id: orgId,
name: newRoleName,
color: newRoleColor,
permissions: newRolePermissions,
position: roles.length,
})
.select()
.single();
if (!error && role) {
roles = [...roles, role as OrgRole];
}
}
showRoleModal = false;
isSavingRole = false;
}
async function deleteRole(role: OrgRole) {
if (role.is_system) return;
if (
!confirm(
`Delete role "${role.name}"? Members with this role will need to be reassigned.`,
)
)
return;
const { error } = await supabase
.from("org_roles")
.delete()
.eq("id", role.id);
if (error) {
toasts.error(m.toast_error_delete_role());
return;
}
roles = roles.filter((r) => r.id !== role.id);
}
function togglePermission(perm: string) {
if (newRolePermissions.includes(perm)) {
newRolePermissions = newRolePermissions.filter((p) => p !== perm);
} else {
newRolePermissions = [...newRolePermissions, perm];
}
}
</script>
<div class="space-y-4 max-w-2xl">
<div class="flex items-center justify-between">
<div>
<h2 class="font-heading text-body text-white">Roles</h2>
<p class="text-body-sm text-light/40 mt-0.5">
Create custom roles with specific permissions.
</p>
</div>
<Button size="sm" onclick={() => openRoleModal()} icon="add">
Create Role
</Button>
</div>
<div class="flex flex-col gap-2">
{#each roles as role}
<div class="bg-dark/30 border border-light/5 rounded-xl px-4 py-3 hover:border-light/10 transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<div
class="w-2.5 h-2.5 rounded-full shrink-0"
style="background-color: {role.color}"
></div>
<span class="text-body-sm font-medium text-white">{role.name}</span>
{#if role.is_system}
<span class="text-[10px] text-light/30 bg-light/5 px-1.5 py-0.5 rounded-md">System</span>
{/if}
{#if role.is_default}
<span class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded-md">Default</span>
{/if}
</div>
<div class="flex items-center gap-1.5">
{#if !role.is_system || role.name !== "Owner"}
<button
type="button"
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
onclick={() => openRoleModal(role)}
title="Edit"
>
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
</button>
{/if}
{#if !role.is_system}
<button
type="button"
class="p-1.5 text-light/40 hover:text-error hover:bg-error/10 rounded-lg transition-colors"
onclick={() => deleteRole(role)}
title="Delete"
>
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">delete</span>
</button>
{/if}
</div>
</div>
<div class="flex flex-wrap gap-1">
{#if role.permissions.includes("*")}
<span class="text-[10px] bg-light/5 text-light/50 px-1.5 py-0.5 rounded-md">All Permissions</span>
{:else}
{#each role.permissions.slice(0, 6) as perm}
<span class="text-[10px] bg-light/5 text-light/40 px-1.5 py-0.5 rounded-md">{perm}</span>
{/each}
{#if role.permissions.length > 6}
<span class="text-[10px] text-light/30">+{role.permissions.length - 6} more</span>
{/if}
{/if}
</div>
</div>
{/each}
</div>
</div>
<!-- Edit/Create Role Modal -->
<Modal
isOpen={showRoleModal}
onClose={() => (showRoleModal = false)}
title={editingRole ? "Edit Role" : "Create Role"}
>
<div class="space-y-4">
<Input
label="Name"
bind:value={newRoleName}
placeholder="e.g., Moderator"
disabled={editingRole?.is_system}
/>
<div>
<label class="block text-sm font-medium text-light mb-2"
>Color</label
>
<div class="flex gap-2">
{#each roleColors as color}
<button
type="button"
class="w-8 h-8 rounded-full transition-transform {newRoleColor ===
color.value
? 'ring-2 ring-white scale-110'
: ''}"
style="background-color: {color.value}"
onclick={() => (newRoleColor = color.value)}
title={color.label}
></button>
{/each}
</div>
</div>
<div>
<label class="block text-sm font-medium text-light mb-2"
>Permissions</label
>
<div class="space-y-3 max-h-64 overflow-y-auto">
{#each permissionGroups as group}
<div class="p-3 bg-light/5 rounded-lg">
<p class="text-sm font-medium text-light mb-2">
{group.name}
</p>
<div class="grid grid-cols-2 gap-2">
{#each group.permissions as perm}
<label
class="flex items-center gap-2 text-sm text-light/70 cursor-pointer"
>
<input
type="checkbox"
checked={newRolePermissions.includes(
perm,
)}
onchange={() => togglePermission(perm)}
class="rounded"
/>
{perm.split(".")[1]}
</label>
{/each}
</div>
</div>
{/each}
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<Button variant="tertiary" onclick={() => (showRoleModal = false)}
>Cancel</Button
>
<Button
onclick={saveRole}
loading={isSavingRole}
disabled={!newRoleName.trim()}
>{editingRole ? "Save" : "Create"}</Button
>
</div>
</div>
</Modal>