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:
@@ -1,8 +1,10 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { projects, expenses, users, categories } from '$lib/server/db/schema.js';
|
||||
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 }) => {
|
||||
await parent();
|
||||
@@ -48,3 +50,40 @@ export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
|
||||
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">
|
||||
import type { PageData } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageData, ActionData } from './$types';
|
||||
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 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>
|
||||
|
||||
<svelte:head>
|
||||
@@ -12,14 +16,68 @@
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="mb-4 flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if editing && canEdit}
|
||||
<form
|
||||
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}
|
||||
</div>
|
||||
{#if canAddExpense}
|
||||
{#if canAddExpense && !editing}
|
||||
<a
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user