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 { 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user