Mega push vol 5, working on messaging now
This commit is contained in:
350
src/lib/components/settings/SettingsRoles.svelte
Normal file
350
src/lib/components/settings/SettingsRoles.svelte
Normal file
@@ -0,0 +1,350 @@
|
||||
<script lang="ts">
|
||||
import { Button, Modal, Card, 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-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-light">Roles</h2>
|
||||
<p class="text-sm text-light/50">
|
||||
Create custom roles with specific permissions.
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={() => openRoleModal()} icon="add">
|
||||
Create Role
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4">
|
||||
{#each roles as role}
|
||||
<Card>
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
style="background-color: {role.color}"
|
||||
></div>
|
||||
<span class="font-medium text-light"
|
||||
>{role.name}</span
|
||||
>
|
||||
{#if role.is_system}
|
||||
<span
|
||||
class="text-xs text-light/40 bg-light/10 px-2 py-0.5 rounded"
|
||||
>System</span
|
||||
>
|
||||
{/if}
|
||||
{#if role.is_default}
|
||||
<span
|
||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded"
|
||||
>Default</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !role.is_system || role.name !== "Owner"}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
onclick={() => openRoleModal(role)}
|
||||
>Edit</Button
|
||||
>
|
||||
{/if}
|
||||
{#if !role.is_system}
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onclick={() => deleteRole(role)}
|
||||
>Delete</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if role.permissions.includes("*")}
|
||||
<span
|
||||
class="text-xs bg-light/10 text-light/70 px-2 py-1 rounded"
|
||||
>All Permissions</span
|
||||
>
|
||||
{:else}
|
||||
{#each role.permissions.slice(0, 6) as perm}
|
||||
<span
|
||||
class="text-xs bg-light/10 text-light/50 px-2 py-1 rounded"
|
||||
>{perm}</span
|
||||
>
|
||||
{/each}
|
||||
{#if role.permissions.length > 6}
|
||||
<span class="text-xs text-light/40"
|
||||
>+{role.permissions.length - 6} more</span
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/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>
|
||||
Reference in New Issue
Block a user