886 lines
24 KiB
Svelte
886 lines
24 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
Button,
|
|
Input,
|
|
Avatar,
|
|
Select,
|
|
Textarea,
|
|
} from "$lib/components/ui";
|
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
import * as m from "$lib/paraglide/messages";
|
|
import type { Database } from "$lib/supabase/types";
|
|
import { toasts } from "$lib/stores/toast.svelte";
|
|
import { invalidateAll } from "$app/navigation";
|
|
import {
|
|
SUPPORTED_CURRENCIES,
|
|
DATE_FORMAT_OPTIONS,
|
|
WEEK_START_OPTIONS,
|
|
CALENDAR_VIEW_OPTIONS,
|
|
TIMEZONE_OPTIONS,
|
|
EVENT_STATUS_OPTIONS,
|
|
DASHBOARD_LAYOUT_OPTIONS,
|
|
DEPT_MODULE_OPTIONS,
|
|
EVENT_COLOR_PRESETS,
|
|
formatCurrency,
|
|
} from "$lib/utils/currency";
|
|
|
|
interface OrgData {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
avatar_url?: string | null;
|
|
description?: string | null;
|
|
theme_color?: string | null;
|
|
currency?: string;
|
|
date_format?: string;
|
|
timezone?: string;
|
|
week_start_day?: string;
|
|
default_calendar_view?: string;
|
|
default_event_color?: string;
|
|
default_event_status?: string;
|
|
default_dept_modules?: string[];
|
|
default_dept_layout?: string;
|
|
feature_chat?: boolean;
|
|
feature_sponsors?: boolean;
|
|
feature_contacts?: boolean;
|
|
feature_budget?: boolean;
|
|
social_links?: Record<string, string>;
|
|
}
|
|
|
|
interface Props {
|
|
supabase: SupabaseClient<Database>;
|
|
org: OrgData;
|
|
isOwner: boolean;
|
|
onLeave: () => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
let { supabase, org, isOwner, onLeave, onDelete }: Props = $props();
|
|
|
|
// ── Organization details ──
|
|
// svelte-ignore state_referenced_locally
|
|
let orgName = $state(org.name);
|
|
// svelte-ignore state_referenced_locally
|
|
let orgSlug = $state(org.slug);
|
|
// svelte-ignore state_referenced_locally
|
|
let avatarUrl = $state(org.avatar_url ?? null);
|
|
// svelte-ignore state_referenced_locally
|
|
let orgDescription = $state(org.description ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let themeColor = $state(org.theme_color ?? "#00a3e0");
|
|
let isSaving = $state(false);
|
|
let isUploading = $state(false);
|
|
let avatarInput = $state<HTMLInputElement | null>(null);
|
|
|
|
// ── Preferences ──
|
|
// svelte-ignore state_referenced_locally
|
|
let currency = $state(org.currency ?? "EUR");
|
|
// svelte-ignore state_referenced_locally
|
|
let dateFormat = $state(org.date_format ?? "DD/MM/YYYY");
|
|
// svelte-ignore state_referenced_locally
|
|
let timezone = $state(org.timezone ?? "Europe/Tallinn");
|
|
// svelte-ignore state_referenced_locally
|
|
let weekStartDay = $state(org.week_start_day ?? "monday");
|
|
// svelte-ignore state_referenced_locally
|
|
let defaultCalendarView = $state(org.default_calendar_view ?? "month");
|
|
let isSavingPrefs = $state(false);
|
|
|
|
// ── Event defaults ──
|
|
// svelte-ignore state_referenced_locally
|
|
let defaultEventColor = $state(org.default_event_color ?? "#7986cb");
|
|
// svelte-ignore state_referenced_locally
|
|
let defaultEventStatus = $state(org.default_event_status ?? "planning");
|
|
// svelte-ignore state_referenced_locally
|
|
let defaultDeptModules = $state<string[]>(
|
|
org.default_dept_modules ?? ["kanban", "files", "checklist"],
|
|
);
|
|
// svelte-ignore state_referenced_locally
|
|
let defaultDeptLayout = $state(org.default_dept_layout ?? "split");
|
|
let isSavingDefaults = $state(false);
|
|
|
|
// ── Social links ──
|
|
// svelte-ignore state_referenced_locally
|
|
let socialWebsite = $state(org.social_links?.website ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialInstagram = $state(org.social_links?.instagram ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialFacebook = $state(org.social_links?.facebook ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialDiscord = $state(org.social_links?.discord ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialLinkedin = $state(org.social_links?.linkedin ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialX = $state(org.social_links?.x ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialYoutube = $state(org.social_links?.youtube ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialTiktok = $state(org.social_links?.tiktok ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialFienta = $state(org.social_links?.fienta ?? "");
|
|
// svelte-ignore state_referenced_locally
|
|
let socialTwitch = $state(org.social_links?.twitch ?? "");
|
|
let isSavingSocial = $state(false);
|
|
|
|
// ── Feature toggles ──
|
|
// svelte-ignore state_referenced_locally
|
|
let featureChat = $state(org.feature_chat ?? true);
|
|
// svelte-ignore state_referenced_locally
|
|
let featureSponsors = $state(org.feature_sponsors ?? true);
|
|
// svelte-ignore state_referenced_locally
|
|
let featureContacts = $state(org.feature_contacts ?? true);
|
|
// svelte-ignore state_referenced_locally
|
|
let featureBudget = $state(org.feature_budget ?? true);
|
|
let isSavingFeatures = $state(false);
|
|
|
|
const currencyPreview = $derived(formatCurrency(1234.56, currency));
|
|
|
|
$effect(() => {
|
|
orgName = org.name;
|
|
orgSlug = org.slug;
|
|
avatarUrl = org.avatar_url ?? null;
|
|
orgDescription = org.description ?? "";
|
|
themeColor = org.theme_color ?? "#00a3e0";
|
|
currency = org.currency ?? "EUR";
|
|
dateFormat = org.date_format ?? "DD/MM/YYYY";
|
|
timezone = org.timezone ?? "Europe/Tallinn";
|
|
weekStartDay = org.week_start_day ?? "monday";
|
|
defaultCalendarView = org.default_calendar_view ?? "month";
|
|
defaultEventColor = org.default_event_color ?? "#7986cb";
|
|
defaultEventStatus = org.default_event_status ?? "planning";
|
|
defaultDeptModules = org.default_dept_modules ?? [
|
|
"kanban",
|
|
"files",
|
|
"checklist",
|
|
];
|
|
defaultDeptLayout = org.default_dept_layout ?? "split";
|
|
socialWebsite = org.social_links?.website ?? "";
|
|
socialInstagram = org.social_links?.instagram ?? "";
|
|
socialFacebook = org.social_links?.facebook ?? "";
|
|
socialDiscord = org.social_links?.discord ?? "";
|
|
socialLinkedin = org.social_links?.linkedin ?? "";
|
|
socialX = org.social_links?.x ?? "";
|
|
socialYoutube = org.social_links?.youtube ?? "";
|
|
socialTiktok = org.social_links?.tiktok ?? "";
|
|
socialFienta = org.social_links?.fienta ?? "";
|
|
socialTwitch = org.social_links?.twitch ?? "";
|
|
featureChat = org.feature_chat ?? true;
|
|
featureSponsors = org.feature_sponsors ?? true;
|
|
featureContacts = org.feature_contacts ?? true;
|
|
featureBudget = org.feature_budget ?? true;
|
|
});
|
|
|
|
async function handleAvatarUpload(e: Event) {
|
|
const input = e.target as HTMLInputElement;
|
|
const file = input.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (!file.type.startsWith("image/")) {
|
|
toasts.error(m.toast_error_select_image());
|
|
return;
|
|
}
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
toasts.error(m.toast_error_image_too_large());
|
|
return;
|
|
}
|
|
|
|
isUploading = true;
|
|
try {
|
|
const ext = file.name.split(".").pop() || "png";
|
|
const path = `org-avatars/${org.id}.${ext}`;
|
|
|
|
const { error: uploadError } = await supabase.storage
|
|
.from("avatars")
|
|
.upload(path, file, { upsert: true });
|
|
|
|
if (uploadError) {
|
|
toasts.error(m.toast_error_upload_avatar());
|
|
return;
|
|
}
|
|
|
|
const { data: urlData } = supabase.storage
|
|
.from("avatars")
|
|
.getPublicUrl(path);
|
|
|
|
const publicUrl = `${urlData.publicUrl}?t=${Date.now()}`;
|
|
|
|
const { error: dbError } = await supabase
|
|
.from("organizations")
|
|
.update({ avatar_url: publicUrl })
|
|
.eq("id", org.id);
|
|
|
|
if (dbError) {
|
|
toasts.error(m.toast_error_save_avatar_url());
|
|
return;
|
|
}
|
|
|
|
avatarUrl = publicUrl;
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_avatar_updated());
|
|
} catch (err) {
|
|
toasts.error(m.toast_error_avatar_upload());
|
|
} finally {
|
|
isUploading = false;
|
|
input.value = "";
|
|
}
|
|
}
|
|
|
|
async function removeAvatar() {
|
|
isSaving = true;
|
|
const { error } = await supabase
|
|
.from("organizations")
|
|
.update({ avatar_url: null })
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_remove_avatar());
|
|
} else {
|
|
avatarUrl = null;
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_avatar_removed());
|
|
}
|
|
isSaving = false;
|
|
}
|
|
|
|
async function saveGeneralSettings() {
|
|
isSaving = true;
|
|
const { error } = await supabase
|
|
.from("organizations")
|
|
.update({
|
|
name: orgName,
|
|
slug: orgSlug,
|
|
description: orgDescription.trim(),
|
|
theme_color: themeColor,
|
|
})
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_save_settings());
|
|
} else if (orgSlug !== org.slug) {
|
|
window.location.href = `/${orgSlug}/settings`;
|
|
} else {
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_settings_saved());
|
|
}
|
|
isSaving = false;
|
|
}
|
|
|
|
async function savePreferences() {
|
|
isSavingPrefs = true;
|
|
const { error } = await supabase
|
|
.from("organizations")
|
|
.update({
|
|
currency,
|
|
date_format: dateFormat,
|
|
timezone,
|
|
week_start_day: weekStartDay,
|
|
default_calendar_view: defaultCalendarView,
|
|
})
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_save_preferences());
|
|
} else {
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_preferences_saved());
|
|
}
|
|
isSavingPrefs = false;
|
|
}
|
|
|
|
async function saveEventDefaults() {
|
|
isSavingDefaults = true;
|
|
const { error } = await supabase
|
|
.from("organizations")
|
|
.update({
|
|
default_event_color: defaultEventColor,
|
|
default_event_status: defaultEventStatus,
|
|
default_dept_modules: defaultDeptModules,
|
|
default_dept_layout: defaultDeptLayout,
|
|
})
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_save_event_defaults());
|
|
} else {
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_event_defaults_saved());
|
|
}
|
|
isSavingDefaults = false;
|
|
}
|
|
|
|
async function saveFeatureToggles() {
|
|
isSavingFeatures = true;
|
|
const { error } = await supabase
|
|
.from("organizations")
|
|
.update({
|
|
feature_chat: featureChat,
|
|
feature_sponsors: featureSponsors,
|
|
feature_contacts: featureContacts,
|
|
feature_budget: featureBudget,
|
|
})
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_save_features());
|
|
} else {
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_features_saved());
|
|
}
|
|
isSavingFeatures = false;
|
|
}
|
|
|
|
async function saveSocialLinks() {
|
|
isSavingSocial = true;
|
|
const links: Record<string, string> = {};
|
|
if (socialWebsite.trim()) links.website = socialWebsite.trim();
|
|
if (socialInstagram.trim()) links.instagram = socialInstagram.trim();
|
|
if (socialFacebook.trim()) links.facebook = socialFacebook.trim();
|
|
if (socialDiscord.trim()) links.discord = socialDiscord.trim();
|
|
if (socialLinkedin.trim()) links.linkedin = socialLinkedin.trim();
|
|
if (socialX.trim()) links.x = socialX.trim();
|
|
if (socialYoutube.trim()) links.youtube = socialYoutube.trim();
|
|
if (socialTiktok.trim()) links.tiktok = socialTiktok.trim();
|
|
if (socialFienta.trim()) links.fienta = socialFienta.trim();
|
|
if (socialTwitch.trim()) links.twitch = socialTwitch.trim();
|
|
|
|
const { error } = await (supabase as any)
|
|
.from("organizations")
|
|
.update({ social_links: links })
|
|
.eq("id", org.id);
|
|
|
|
if (error) {
|
|
toasts.error(m.toast_error_save_social());
|
|
} else {
|
|
await invalidateAll();
|
|
toasts.success(m.toast_success_social_saved());
|
|
}
|
|
isSavingSocial = false;
|
|
}
|
|
|
|
function toggleModule(mod: string) {
|
|
if (defaultDeptModules.includes(mod)) {
|
|
defaultDeptModules = defaultDeptModules.filter((m) => m !== mod);
|
|
} else {
|
|
defaultDeptModules = [...defaultDeptModules, mod];
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-6 max-w-2xl">
|
|
<!-- Organization Details -->
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
|
|
>
|
|
<h2 class="font-heading text-body text-white">Organization details</h2>
|
|
|
|
<!-- Avatar Upload -->
|
|
<div class="flex flex-col gap-2">
|
|
<span class="font-body text-body-sm text-light/60">Avatar</span>
|
|
<div class="flex items-center gap-4">
|
|
<Avatar name={orgName || "?"} src={avatarUrl} size="lg" />
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
class="hidden"
|
|
bind:this={avatarInput}
|
|
onchange={handleAvatarUpload}
|
|
/>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onclick={() => avatarInput?.click()}
|
|
loading={isUploading}
|
|
>
|
|
Upload
|
|
</Button>
|
|
{#if avatarUrl}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onclick={removeAvatar}
|
|
>
|
|
Remove
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Input
|
|
label="Name"
|
|
bind:value={orgName}
|
|
placeholder="Organization name"
|
|
/>
|
|
<Input
|
|
label="URL slug (yoursite.com/...)"
|
|
bind:value={orgSlug}
|
|
placeholder="my-org"
|
|
/>
|
|
<Textarea
|
|
variant="compact"
|
|
label="Description"
|
|
bind:value={orgDescription}
|
|
placeholder="What does your organization do?"
|
|
rows={2}
|
|
resize="none"
|
|
/>
|
|
|
|
<!-- Theme Color (hidden for now) -->
|
|
<!-- <div class="flex flex-col gap-2">
|
|
<span class="font-body text-body-sm text-light/60">Theme color</span>
|
|
<div class="flex items-center gap-3">
|
|
<label
|
|
class="w-8 h-8 rounded-lg cursor-pointer overflow-hidden border border-light/10"
|
|
style="background-color: {themeColor}"
|
|
>
|
|
<input type="color" bind:value={themeColor} class="opacity-0 w-0 h-0" />
|
|
</label>
|
|
<span class="text-[12px] text-light/40 font-mono">{themeColor}</span>
|
|
</div>
|
|
</div> -->
|
|
|
|
<div>
|
|
<Button size="sm" onclick={saveGeneralSettings} loading={isSaving}
|
|
>Save Changes</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Social & Links -->
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
|
|
>
|
|
<div>
|
|
<h2 class="font-heading text-body text-white">
|
|
{m.settings_social_title()}
|
|
</h2>
|
|
<p class="text-[11px] text-light/40 mt-0.5">
|
|
{m.settings_social_desc()}
|
|
</p>
|
|
</div>
|
|
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_website()}
|
|
bind:value={socialWebsite}
|
|
placeholder={m.settings_social_website_placeholder()}
|
|
icon="language"
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_instagram()}
|
|
bind:value={socialInstagram}
|
|
placeholder={m.settings_social_instagram_placeholder()}
|
|
/>
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_facebook()}
|
|
bind:value={socialFacebook}
|
|
placeholder={m.settings_social_facebook_placeholder()}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_discord()}
|
|
bind:value={socialDiscord}
|
|
placeholder={m.settings_social_discord_placeholder()}
|
|
/>
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_linkedin()}
|
|
bind:value={socialLinkedin}
|
|
placeholder={m.settings_social_linkedin_placeholder()}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_x()}
|
|
bind:value={socialX}
|
|
placeholder={m.settings_social_x_placeholder()}
|
|
/>
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_youtube()}
|
|
bind:value={socialYoutube}
|
|
placeholder={m.settings_social_youtube_placeholder()}
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_tiktok()}
|
|
bind:value={socialTiktok}
|
|
placeholder={m.settings_social_tiktok_placeholder()}
|
|
/>
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_fienta()}
|
|
bind:value={socialFienta}
|
|
placeholder={m.settings_social_fienta_placeholder()}
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
variant="compact"
|
|
type="url"
|
|
label={m.settings_social_twitch()}
|
|
bind:value={socialTwitch}
|
|
placeholder={m.settings_social_twitch_placeholder()}
|
|
/>
|
|
|
|
<div>
|
|
<Button size="sm" onclick={saveSocialLinks} loading={isSavingSocial}
|
|
>{m.settings_social_save()}</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preferences -->
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
|
|
>
|
|
<div>
|
|
<h2 class="font-heading text-body text-white">Preferences</h2>
|
|
<p class="text-[11px] text-light/40 mt-0.5">
|
|
Currency, date format, timezone, and calendar defaults.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Currency -->
|
|
<Select
|
|
variant="compact"
|
|
label="Currency"
|
|
bind:value={currency}
|
|
placeholder=""
|
|
options={SUPPORTED_CURRENCIES.map((c) => ({
|
|
value: c.code,
|
|
label: c.label,
|
|
}))}
|
|
hint={`Preview: ${currencyPreview}`}
|
|
/>
|
|
|
|
<!-- Date Format -->
|
|
<Select
|
|
variant="compact"
|
|
label="Date format"
|
|
bind:value={dateFormat}
|
|
placeholder=""
|
|
options={DATE_FORMAT_OPTIONS}
|
|
/>
|
|
|
|
<!-- Timezone -->
|
|
<Select
|
|
variant="compact"
|
|
label="Timezone"
|
|
bind:value={timezone}
|
|
placeholder=""
|
|
groups={TIMEZONE_OPTIONS.map((g) => ({
|
|
label: g.group,
|
|
options: g.zones.map((z) => ({
|
|
value: z,
|
|
label: z.replace(/_/g, " "),
|
|
})),
|
|
}))}
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<Select
|
|
variant="compact"
|
|
label="Week starts on"
|
|
bind:value={weekStartDay}
|
|
placeholder=""
|
|
options={WEEK_START_OPTIONS}
|
|
/>
|
|
<Select
|
|
variant="compact"
|
|
label="Default calendar view"
|
|
bind:value={defaultCalendarView}
|
|
placeholder=""
|
|
options={CALENDAR_VIEW_OPTIONS}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Button size="sm" onclick={savePreferences} loading={isSavingPrefs}
|
|
>Save Preferences</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Event Defaults -->
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
|
|
>
|
|
<div>
|
|
<h2 class="font-heading text-body text-white">Event defaults</h2>
|
|
<p class="text-[11px] text-light/40 mt-0.5">
|
|
Defaults applied when creating new events and departments.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Default Event Color -->
|
|
<div class="flex flex-col gap-2">
|
|
<span class="font-body text-body-sm text-light/60"
|
|
>Default event color</span
|
|
>
|
|
<div class="flex flex-wrap gap-2">
|
|
{#each EVENT_COLOR_PRESETS as color}
|
|
<button
|
|
type="button"
|
|
class="w-7 h-7 rounded-lg border-2 transition-all {defaultEventColor ===
|
|
color
|
|
? 'border-white scale-110'
|
|
: 'border-transparent hover:border-light/30'}"
|
|
style="background-color: {color}"
|
|
onclick={() => (defaultEventColor = color)}
|
|
aria-label="Color {color}"
|
|
></button>
|
|
{/each}
|
|
<label
|
|
class="w-7 h-7 rounded-lg border-2 border-dashed border-light/20 hover:border-light/40 transition-all cursor-pointer flex items-center justify-center overflow-hidden"
|
|
title="Custom color"
|
|
>
|
|
<input
|
|
type="color"
|
|
class="opacity-0 absolute w-0 h-0"
|
|
bind:value={defaultEventColor}
|
|
/>
|
|
<span
|
|
class="material-symbols-rounded text-light/30"
|
|
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
|
>colorize</span
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Default Event Status -->
|
|
<Select
|
|
variant="compact"
|
|
label="Default event status"
|
|
bind:value={defaultEventStatus}
|
|
placeholder=""
|
|
options={EVENT_STATUS_OPTIONS}
|
|
/>
|
|
|
|
<!-- Default Department Layout -->
|
|
<Select
|
|
variant="compact"
|
|
label="Default department layout"
|
|
bind:value={defaultDeptLayout}
|
|
placeholder=""
|
|
options={DASHBOARD_LAYOUT_OPTIONS}
|
|
/>
|
|
|
|
<!-- Default Department Modules -->
|
|
<div class="flex flex-col gap-2">
|
|
<span class="font-body text-body-sm text-light/60"
|
|
>Default department modules</span
|
|
>
|
|
<p class="text-[11px] text-light/30">
|
|
Modules auto-added when a new department is created.
|
|
</p>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
{#each DEPT_MODULE_OPTIONS as mod}
|
|
<button
|
|
type="button"
|
|
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl border transition-all text-left {defaultDeptModules.includes(
|
|
mod.value,
|
|
)
|
|
? 'bg-primary/10 border-primary/30 text-white'
|
|
: 'bg-dark/30 border-light/5 text-light/40 hover:border-light/10'}"
|
|
onclick={() => toggleModule(mod.value)}
|
|
>
|
|
<span
|
|
class="material-symbols-rounded {defaultDeptModules.includes(
|
|
mod.value,
|
|
)
|
|
? 'text-primary'
|
|
: 'text-light/30'}"
|
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
|
>{mod.icon}</span
|
|
>
|
|
<span class="text-body-sm font-body">{mod.label}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Button
|
|
size="sm"
|
|
onclick={saveEventDefaults}
|
|
loading={isSavingDefaults}>Save Event Defaults</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feature Toggles -->
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
|
|
>
|
|
<div>
|
|
<h2 class="font-heading text-body text-white">Features</h2>
|
|
<p class="text-[11px] text-light/40 mt-0.5">
|
|
Enable or disable features for this organization.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<!-- Chat toggle disabled until fully developed -->
|
|
<!-- <label
|
|
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="material-symbols-rounded text-purple-400"
|
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
|
>chat</span
|
|
>
|
|
<div>
|
|
<p class="text-body-sm text-white">Chat</p>
|
|
<p class="text-[11px] text-light/30">
|
|
Real-time messaging via Matrix
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={featureChat}
|
|
class="w-4 h-4 rounded accent-primary"
|
|
/>
|
|
</label> -->
|
|
|
|
<label
|
|
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="material-symbols-rounded text-emerald-400"
|
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
|
>account_balance</span
|
|
>
|
|
<div>
|
|
<p class="text-body-sm text-white">Budget & Finances</p>
|
|
<p class="text-[11px] text-light/30">
|
|
Income/expense tracking, planned vs actual budgets
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={featureBudget}
|
|
class="w-4 h-4 rounded accent-primary"
|
|
/>
|
|
</label>
|
|
|
|
<label
|
|
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="material-symbols-rounded text-indigo-400"
|
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
|
>handshake</span
|
|
>
|
|
<div>
|
|
<p class="text-body-sm text-white">Sponsors</p>
|
|
<p class="text-[11px] text-light/30">
|
|
Sponsor CRM with tiers, deliverables, and fund
|
|
tracking
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={featureSponsors}
|
|
class="w-4 h-4 rounded accent-primary"
|
|
/>
|
|
</label>
|
|
|
|
<label
|
|
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="material-symbols-rounded text-blue-400"
|
|
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
|
>contacts</span
|
|
>
|
|
<div>
|
|
<p class="text-body-sm text-white">Contacts</p>
|
|
<p class="text-[11px] text-light/30">
|
|
Vendor and contact directory per department
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={featureContacts}
|
|
class="w-4 h-4 rounded accent-primary"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<Button
|
|
size="sm"
|
|
onclick={saveFeatureToggles}
|
|
loading={isSavingFeatures}>Save Features</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Danger Zone -->
|
|
{#if isOwner}
|
|
<div
|
|
class="bg-dark/30 border border-error/10 rounded-xl p-5 flex flex-col gap-3"
|
|
>
|
|
<h4 class="font-heading text-body-sm text-error">Danger Zone</h4>
|
|
<p class="font-body text-[11px] text-light/40">
|
|
Permanently delete this organization and all its data.
|
|
</p>
|
|
<div>
|
|
<Button variant="danger" size="sm" onclick={onDelete}
|
|
>Delete Organization</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Leave Organization (non-owners) -->
|
|
{#if !isOwner}
|
|
<div
|
|
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-3"
|
|
>
|
|
<h4 class="font-heading text-body-sm text-white">
|
|
Leave Organization
|
|
</h4>
|
|
<p class="font-body text-[11px] text-light/40">
|
|
Leave this organization. You will need to be re-invited to
|
|
rejoin.
|
|
</p>
|
|
<div>
|
|
<Button variant="secondary" size="sm" onclick={onLeave}
|
|
>Leave {org.name}</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|