diff --git a/apps/web/src/routes/(app)/collective/manage/+page.svelte b/apps/web/src/routes/(app)/collective/manage/+page.svelte
index e2938c0..fc66cb6 100644
--- a/apps/web/src/routes/(app)/collective/manage/+page.svelte
+++ b/apps/web/src/routes/(app)/collective/manage/+page.svelte
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { getSupabase } from '$lib/supabase';
import { currentUser } from '$lib/stores/auth';
- import { currentCollective, collectiveMembers } from '$lib/stores/collective';
+ import { currentCollective, collectiveMembers, userCollectives } from '$lib/stores/collective';
import Avatar from '$lib/components/Avatar.svelte';
import * as m from '$lib/paraglide/messages';
@@ -143,6 +143,75 @@
if (role === 'member') return m.role_member();
return m.role_guest();
}
+
+ // ── Fase 10.2: rename + change emoji (admin only) ───────────────────────
+ let nameDraft = $state('');
+ let nameSaving = $state(false);
+ let nameSaved = $state(false);
+ let emojiPickerOpen = $state(false);
+
+ const EMOJI_OPTIONS = [
+ '🏠', '🏡', '🏢', '🏣', '🏤', '🏥', '🏦', '🏨',
+ '🏩', '🏪', '🏫', '🏬', '🏭', '🏯', '🏰', '🗼',
+ '🌆', '🌇', '🌃', '🌉', '🏙️', '⛺', '🌴', '🌵'
+ ];
+
+ // Keep nameDraft in sync with $currentCollective.
+ $effect(() => {
+ if ($currentCollective) nameDraft = $currentCollective.name;
+ });
+
+ async function saveCollectiveName() {
+ if (!$currentCollective || !isAdmin) return;
+ const trimmed = nameDraft.trim();
+ if (!trimmed || trimmed === $currentCollective.name) return;
+ nameSaving = true;
+ const { data, error: updErr } = await getSupabase()
+ .from('collectives')
+ .update({ name: trimmed })
+ .eq('id', $currentCollective.id)
+ .select('id, name, emoji, created_at')
+ .single();
+ nameSaving = false;
+ if (updErr || !data) {
+ error = updErr?.message ?? 'Failed to save name.';
+ return;
+ }
+ applyCollectivePatch(data as { id: string; name: string; emoji: string; created_at: string });
+ nameSaved = true;
+ setTimeout(() => (nameSaved = false), 1500);
+ }
+
+ function onNameKeydown(e: KeyboardEvent) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ void saveCollectiveName();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ if ($currentCollective) nameDraft = $currentCollective.name;
+ }
+ }
+
+ async function selectEmoji(emoji: string) {
+ emojiPickerOpen = false;
+ if (!$currentCollective || !isAdmin || emoji === $currentCollective.emoji) return;
+ const { data, error: updErr } = await getSupabase()
+ .from('collectives')
+ .update({ emoji })
+ .eq('id', $currentCollective.id)
+ .select('id, name, emoji, created_at')
+ .single();
+ if (updErr || !data) {
+ error = updErr?.message ?? 'Failed to save emoji.';
+ return;
+ }
+ applyCollectivePatch(data as { id: string; name: string; emoji: string; created_at: string });
+ }
+
+ function applyCollectivePatch(c: { id: string; name: string; emoji: string; created_at: string }) {
+ currentCollective.set(c);
+ userCollectives.update((list) => list.map((it) => (it.id === c.id ? c : it)));
+ }
@@ -161,6 +230,72 @@
{error}
{/if}
+
+ {#if isAdmin && $currentCollective}
+
+
+
+
+
+
+ void saveCollectiveName()}
+ maxlength="60"
+ class="w-full rounded-lg border border-slate-300 bg-surface px-3 py-2 text-sm
+ focus:border-slate-500 focus:outline-none dark:border-slate-600 dark:bg-slate-800"
+ />
+ {#if nameSaving}
+ {m.settings_saving()}
+ {:else if nameSaved}
+ ✓
+ {/if}
+
+
+
+
+ {#if emojiPickerOpen}
+
+ {#each EMOJI_OPTIONS as e}
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/web/tests/e2e/rename-collective.test.ts b/apps/web/tests/e2e/rename-collective.test.ts
new file mode 100644
index 0000000..8f5597d
--- /dev/null
+++ b/apps/web/tests/e2e/rename-collective.test.ts
@@ -0,0 +1,65 @@
+/**
+ * MC-01 (rescued from Fase 7) — /collective/manage rename + emoji change.
+ *
+ * Admin (Ana) edits name + emoji inline; the value persists across reload.
+ * Member (Borja) does not see the editable inputs.
+ *
+ * Cleanup: restore the seed name + emoji in afterAll so downstream suites
+ * that hard-match "Casa García-López" / "🏠" don't break.
+ */
+import { test, expect } from '@playwright/test';
+import { USERS } from '../fixtures/users.js';
+import { loginAs } from '../fixtures/login.js';
+import { closePool, sql, COLLECTIVE_ID } from '@colectivo/test-utils';
+
+test.describe.configure({ mode: 'serial' });
+
+test.describe('Rename collective (CU-H07)', () => {
+ test.afterAll(async () => {
+ await sql(`UPDATE public.collectives SET name = $1, emoji = $2 WHERE id = $3`, [
+ 'Casa García-López',
+ '🏠',
+ COLLECTIVE_ID
+ ]);
+ await closePool();
+ });
+
+ test('MC-01a: admin renames + changes emoji; value persists across reload', async ({
+ page
+ }) => {
+ await loginAs(page, USERS.ana);
+ await page.goto('/collective/manage');
+
+ const newName = `Casa renamed ${Date.now()}`;
+ const nameInput = page.getByTestId('collective-name-edit');
+ await expect(nameInput).toBeVisible({ timeout: 15_000 });
+ await nameInput.fill(newName);
+ await page.getByTestId('collective-name-save').click();
+
+ // Pick a different emoji.
+ await page.getByTestId('collective-emoji-toggle').click();
+ await page.getByTestId('collective-emoji-option-🏡').click();
+
+ // Reload and verify the inputs reflect the persisted value.
+ await page.reload();
+ await expect(page.getByTestId('collective-name-edit')).toHaveValue(newName, {
+ timeout: 15_000
+ });
+
+ const row = await sql('SELECT name, emoji FROM public.collectives WHERE id = $1', [
+ COLLECTIVE_ID
+ ]);
+ expect(row.rows[0].name).toBe(newName);
+ expect(row.rows[0].emoji).toBe('🏡');
+ });
+
+ test('MC-01b: non-admin (Borja) does not see editable inputs', async ({ page }) => {
+ await loginAs(page, USERS.borja);
+ await page.goto('/collective/manage');
+
+ // Members list loads → page is hydrated; editable name input must be absent.
+ await expect(page.getByTestId(`member-row-${USERS.ana.id}`)).toBeVisible({ timeout: 15_000 });
+ await expect(page.getByTestId('collective-name-edit')).toHaveCount(0);
+ await expect(page.getByTestId('collective-emoji-toggle')).toHaveCount(0);
+ });
+});