Add inline rename/edit on project detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 16:09:52 +07:00
parent 57f3d42133
commit fef69b653c
2 changed files with 107 additions and 10 deletions
@@ -1,8 +1,10 @@
import { error } from '@sveltejs/kit'; import { error, fail } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js'; import { db } from '$lib/server/db/index.js';
import { projects, expenses, users, categories } from '$lib/server/db/schema.js'; import { projects, expenses, users, categories } from '$lib/server/db/schema.js';
import { eq, and, sql } from 'drizzle-orm'; import { eq, and, sql } from 'drizzle-orm';
import { requireCompanyRole } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js';
export const load: PageServerLoad = async ({ params, parent }) => { export const load: PageServerLoad = async ({ params, parent }) => {
await parent(); await parent();
@@ -48,3 +50,40 @@ export const load: PageServerLoad = async ({ params, parent }) => {
return { project, expenses: expenseList, stats }; return { project, expenses: expenseList, stats };
}; };
export const actions: Actions = {
updateProject: async ({ request, locals, params }) => {
const { user } = await requireCompanyRole(locals, params.companyId, 'manager');
const fd = await request.formData();
const name = fd.get('name')?.toString().trim();
const description = fd.get('description')?.toString().trim() || null;
if (!name) return fail(400, { action: 'updateProject', error: 'Project name is required' });
const [existing] = await db
.select({ id: projects.id, name: projects.name })
.from(projects)
.where(and(eq(projects.id, params.projectId), eq(projects.companyId, params.companyId)))
.limit(1);
if (!existing) error(404, 'Project not found');
await db
.update(projects)
.set({ name, description, updatedAt: new Date() })
.where(eq(projects.id, params.projectId));
const renamed = existing.name !== name;
await logCompanyEvent(
params.companyId,
user.id,
'project_updated',
renamed
? `Project renamed from "${existing.name}" to "${name}"`
: `Project "${name}" updated`,
{ projectId: params.projectId, previousName: existing.name, newName: name }
);
return { success: true, action: 'updateProject' };
}
};
@@ -1,10 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
import { formatCurrency } from '$lib/utils/currency.js'; import { formatCurrency } from '$lib/utils/currency.js';
let { data } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
const currency = $derived(data.company.currency); const currency = $derived(data.company.currency);
const canAddExpense = $derived(data.companyRoles.some(r => r === 'admin' || r === 'manager' || r === 'user' || r === 'hr')); const canAddExpense = $derived(data.companyRoles.some(r => r === 'admin' || r === 'manager' || r === 'user' || r === 'hr'));
const canEdit = $derived(data.companyRoles.some(r => r === 'admin' || r === 'manager'));
let editing = $state(false);
</script> </script>
<svelte:head> <svelte:head>
@@ -12,14 +16,68 @@
</svelte:head> </svelte:head>
<div> <div>
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-start justify-between gap-3">
<div> <div class="min-w-0 flex-1">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{data.project.name}</h2> {#if editing && canEdit}
{#if data.project.description} <form
<p class="text-sm text-gray-500 dark:text-gray-400">{data.project.description}</p> method="POST"
action="?/updateProject"
use:enhance={() => async ({ result, update }) => {
await update({ reset: false });
if (result.type === 'success') editing = false;
}}
class="space-y-2"
>
<input
name="name"
type="text"
required
value={data.project.name}
class="w-full rounded-md border border-gray-300 px-3 py-2 text-lg font-semibold dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<textarea
name="description"
rows="2"
placeholder="Description (optional)"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">{data.project.description ?? ''}</textarea>
<div class="flex gap-2">
<button
type="submit"
class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
>
Save
</button>
<button
type="button"
onclick={() => (editing = false)}
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
>
Cancel
</button>
</div>
{#if form?.action === 'updateProject' && form.error}
<p class="text-sm text-red-600 dark:text-red-400">{form.error}</p>
{/if}
</form>
{:else}
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{data.project.name}</h2>
{#if canEdit}
<button
type="button"
onclick={() => (editing = true)}
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
Edit
</button>
{/if}
</div>
{#if data.project.description}
<p class="text-sm text-gray-500 dark:text-gray-400">{data.project.description}</p>
{/if}
{/if} {/if}
</div> </div>
{#if canAddExpense} {#if canAddExpense && !editing}
<a <a
href="/companies/{data.company.id}/projects/{data.project.id}/expenses/new" href="/companies/{data.company.id}/projects/{data.project.id}/expenses/new"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"