desker/apps/web/src/routes/admin/organization/+page.svelte

201 lines
6.5 KiB
Svelte

<script lang="ts">
import { organization, loadOrganization } from "$lib/stores/organization";
import { user } from "$lib/stores/auth";
import { apiFetch } from "$lib/api/client";
import { onMount } from "svelte";
import { resolveAssetUrl } from "$lib/utils/url";
import { t } from "svelte-i18n";
let name = "";
let logoUrl = "";
let language = "en";
let logoFile: File | null = null;
let loading = false;
let success = false;
let error = "";
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
let logoUrlInput: HTMLInputElement;
let logoFileInput: HTMLInputElement;
onMount(async () => {
if ($user?.organizationId) {
await loadOrganization($user.organizationId);
if ($organization) {
name = $organization.name;
logoUrl = $organization.logoUrl || "";
language = $organization.language || "en";
}
}
});
async function handleSubmit() {
const orgId = $user?.organizationId;
if (!orgId) {
error = "User organization ID is missing";
return;
}
loading = true;
success = false;
error = "";
try {
let finalLogoUrl = logoUrl;
if (logoFile) {
const formData = new FormData();
formData.append('file', logoFile);
const assetResponse = await apiFetch<any>(`/assets/organization/${orgId}/logo`, {
method: 'POST',
body: formData,
});
// Update local logoUrl with the actual URL from the backend
finalLogoUrl = assetResponse.logoUrl;
logoUrl = finalLogoUrl;
}
const updated = await apiFetch<any>(`/organizations/${orgId}`, {
method: 'PATCH',
body: JSON.stringify({ name, logoUrl: finalLogoUrl || "", language }),
});
organization.set(updated);
success = true;
logoFile = null;
} catch (e: any) {
error = e.message || "Failed to update organization";
} finally {
loading = false;
}
}
function handleFileChange(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
if (file.size > MAX_FILE_SIZE) {
error = "File size exceeds 10MB limit";
target.value = "";
return;
}
logoFile = file;
// If a file is selected, clear the logoUrl to avoid ambiguity
logoUrl = "";
error = "";
}
}
function handleUrlChange() {
// If the user starts typing a URL, clear the file selection
if (logoFile) {
logoFile = null;
if (logoFileInput) {
logoFileInput.value = "";
}
}
}
// Reactive preview URL
$: previewUrl = logoFile ? URL.createObjectURL(logoFile) : resolveAssetUrl(logoUrl);
function handleRemoveLogo() {
logoUrl = "";
logoFile = null;
}
</script>
<div class="max-w-2xl mx-auto py-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">{$t('organization.settings')}</h2>
<div class="bg-white shadow rounded-lg p-6">
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
<div>
<label for="orgName" class="block text-sm font-medium text-gray-700">{$t('organization.name')}</label>
<input
id="orgName"
type="text"
bind:value={name}
required
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter organization name"
/>
</div>
<div>
<label for="orgLanguage" class="block text-sm font-medium text-gray-700">{$t('common.language')}</label>
<select
id="orgLanguage"
bind:value={language}
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="en">English</option>
<option value="es-ES">Español (Ibérico)</option>
</select>
</div>
<div>
<label for="logoFile" class="block text-sm font-medium text-gray-700">{$t('organization.logo')}</label>
<div class="mt-1 flex items-center space-x-4">
<input
id="logoFile"
type="file"
accept="image/*"
bind:this={logoFileInput}
on:change={handleFileChange}
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
/>
</div>
<p class="mt-2 text-sm text-gray-500">Upload a logo for your organization (max 10MB).</p>
</div>
<div>
<label for="logoUrl" class="block text-sm font-medium text-gray-700">Logo URL (Optional)</label>
<input
id="logoUrl"
type="text"
bind:value={logoUrl}
on:input={handleUrlChange}
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="https://example.com/logo.png"
/>
<p class="mt-2 text-sm text-gray-500">Or provide a URL for your organization's logo.</p>
</div>
{#if previewUrl}
<div class="mt-4">
<div class="flex items-center justify-between mb-2">
<p class="text-sm font-medium text-gray-700">Logo Preview:</p>
<button
type="button"
on:click={handleRemoveLogo}
class="text-sm text-red-600 hover:text-red-800"
>
Remove Logo
</button>
</div>
<div class="w-20 h-20 border rounded-lg flex items-center justify-center overflow-hidden bg-gray-50">
<img src={previewUrl} alt="Logo preview" class="max-w-full max-h-full object-contain" />
</div>
</div>
{/if}
{#if error}
<div class="text-red-600 text-sm mt-2">{error}</div>
{/if}
{#if success}
<div class="text-green-600 text-sm mt-2">{$t('organization.updateSuccess')}</div>
{/if}
<div class="flex justify-end">
<button
type="submit"
disabled={loading}
class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{loading ? $t('common.loading') : $t('common.save')}
</button>
</div>
</form>
</div>
</div>