diff --git a/apps/web/public/og-image.png b/apps/web/public/og-image.png new file mode 100644 index 0000000..0ff5257 Binary files /dev/null and b/apps/web/public/og-image.png differ diff --git a/apps/web/src/app/(app)/admin/vacations/page.tsx b/apps/web/src/app/(app)/admin/vacations/page.tsx index 8037834..fdf5a96 100644 --- a/apps/web/src/app/(app)/admin/vacations/page.tsx +++ b/apps/web/src/app/(app)/admin/vacations/page.tsx @@ -1,7 +1,7 @@ import { PublicHolidayBatch } from "~/components/vacations/PublicHolidayBatch.js"; import { EntitlementManager } from "~/components/vacations/EntitlementManager.js"; -export const metadata = { title: "Vacation Management — Planarchy" }; +export const metadata = { title: "Vacation Management — plANARCHY" }; export default function AdminVacationsPage() { return ( diff --git a/apps/web/src/app/(app)/analytics/computation-graph/page.tsx b/apps/web/src/app/(app)/analytics/computation-graph/page.tsx new file mode 100644 index 0000000..8938d91 --- /dev/null +++ b/apps/web/src/app/(app)/analytics/computation-graph/page.tsx @@ -0,0 +1,5 @@ +import ComputationGraphClient from "~/components/analytics/ComputationGraphClient"; + +export default function ComputationGraphPage() { + return ; +} diff --git a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx index a145fc1..5e8bd0c 100644 --- a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx +++ b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { EstimateStatus, type EstimateVersionStatus } from "@planarchy/shared"; import { clsx } from "clsx"; import { EstimateWizard } from "~/components/estimates/EstimateWizard.js"; +import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { usePermissions } from "~/hooks/usePermissions.js"; import { formatDateLong, formatMoney } from "~/lib/format.js"; import { trpc } from "~/lib/trpc/client.js"; @@ -121,7 +122,7 @@ function EstimateDetailPanel({

- Estimate detail + Estimate detail

{estimate.name} @@ -205,7 +206,7 @@ function EstimateDetailPanel({

- Scope items + Scope items

{latestVersion.scopeItems.length}
@@ -238,7 +239,7 @@ function EstimateDetailPanel({

- Demand lines + Demand lines

{latestVersion.demandLines.length}
@@ -344,13 +345,13 @@ function EstimateCard({
-

Opportunity

+

Opportunity

{estimate.opportunityId ?? "Not set"}

-

Updated

+

Updated

{formatDateLong(estimate.updatedAt)}

@@ -465,7 +466,7 @@ export function EstimatesClient() { No estimates yet

- Start with the wizard to create a connected estimate from Planarchy data. + Start with the wizard to create a connected estimate from plANARCHY data.

) : ( diff --git a/apps/web/src/app/(app)/projects/ProjectsClient.tsx b/apps/web/src/app/(app)/projects/ProjectsClient.tsx index 8b650fe..2aa3064 100644 --- a/apps/web/src/app/(app)/projects/ProjectsClient.tsx +++ b/apps/web/src/app/(app)/projects/ProjectsClient.tsx @@ -162,6 +162,8 @@ interface ProjectRow { totalPersonDays: number; utilizationPercent: number; dynamicFields?: Record | null; + coverImageUrl?: string | null; + color?: string | null; } // ─── Main component ─────────────────────────────────────────────────────────── @@ -351,8 +353,21 @@ export function ProjectsClient() { case "name": return ( - - {project.name} + + {project.coverImageUrl ? ( + + ) : ( + + {project.name.split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase()} + + )} + {project.name} ); diff --git a/apps/web/src/app/(app)/projects/[id]/page.tsx b/apps/web/src/app/(app)/projects/[id]/page.tsx index 73db05c..4d663eb 100644 --- a/apps/web/src/app/(app)/projects/[id]/page.tsx +++ b/apps/web/src/app/(app)/projects/[id]/page.tsx @@ -2,11 +2,16 @@ import { notFound } from "next/navigation"; import { formatDate } from "~/lib/format.js"; import Link from "next/link"; import { createCaller } from "~/server/trpc.js"; +import { auth } from "~/server/auth.js"; import { BudgetStatusCard } from "~/components/projects/BudgetStatusCard.js"; import { ProjectDetailActions } from "~/components/projects/ProjectDetailClient.js"; import { ProjectDemandsTable } from "~/components/projects/ProjectDemandsTable.js"; import { ProjectAssignmentsTable } from "~/components/projects/ProjectAssignmentsTable.js"; import { PROJECT_STATUS_BADGE as STATUS_COLORS, ORDER_TYPE_BADGE as ORDER_TYPE_COLORS } from "~/lib/status-styles.js"; +import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; +import { CoverArtSection } from "~/components/projects/CoverArtSection.js"; + +const EDIT_ROLES = new Set(["ADMIN", "MANAGER"]); interface ProjectDetailPageProps { params: Promise<{ id: string }>; @@ -14,7 +19,9 @@ interface ProjectDetailPageProps { export default async function ProjectDetailPage({ params }: ProjectDetailPageProps) { const { id } = await params; - const trpc = await createCaller(); + const [trpc, session] = await Promise.all([createCaller(), auth()]); + const userRole = (session?.user as { role?: string } | undefined)?.role ?? "USER"; + const canEditProject = EDIT_ROLES.has(userRole); let project: Awaited>; try { @@ -41,8 +48,18 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro Back to Projects + {/* Cover Art */} + + {/* Project header */} -
+
@@ -50,9 +67,11 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro {project.status} + {project.orderType} +

{project.name}

@@ -63,7 +82,7 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro {" — "} {formatDate(project.endDate)}
-
Win probability: {project.winProbability}%
+
Win probability: {project.winProbability}%
@@ -71,30 +90,30 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
-
Chargecode
+
Chargecode
{project.shortCode}
-
Order Type
+
Order Type
{project.orderType}
-
Allocation Type
+
Allocation Type
{project.allocationType}
-
Assignments
+
Assignments
{activeAssignments.length} active
-
Open Demands
+
Open Demands
{activeDemands.length} items · {unfilledSeats}/{requestedSeats} seats unfilled
{project.responsiblePerson && (
-
Responsible Person
+
Responsible Person
{project.responsiblePerson}
)} diff --git a/apps/web/src/app/(app)/resources/ResourcesClient.tsx b/apps/web/src/app/(app)/resources/ResourcesClient.tsx index 438f859..01500b5 100644 --- a/apps/web/src/app/(app)/resources/ResourcesClient.tsx +++ b/apps/web/src/app/(app)/resources/ResourcesClient.tsx @@ -978,7 +978,7 @@ export function ResourcesClient() { sortField={sortField} sortDir={sortDir} onSort={toggle} - tooltip="Unique employee identifier used across all Planarchy records." + tooltip="Unique employee identifier used across all plANARCHY records." /> ); case "displayName": diff --git a/apps/web/src/app/(app)/resources/[id]/page.tsx b/apps/web/src/app/(app)/resources/[id]/page.tsx index e96326b..7d0d6ec 100644 --- a/apps/web/src/app/(app)/resources/[id]/page.tsx +++ b/apps/web/src/app/(app)/resources/[id]/page.tsx @@ -9,9 +9,9 @@ export async function generateMetadata( try { const trpc = await createCaller(); const resource = await trpc.resource.getById({ id }); - return { title: `${resource.displayName} — Resources | Planarchy` }; + return { title: `${resource.displayName} — Resources | plANARCHY` }; } catch { - return { title: "Resource — Planarchy" }; + return { title: "Resource — plANARCHY" }; } } diff --git a/apps/web/src/app/(app)/vacations/my/page.tsx b/apps/web/src/app/(app)/vacations/my/page.tsx index 829bc04..c5c5279 100644 --- a/apps/web/src/app/(app)/vacations/my/page.tsx +++ b/apps/web/src/app/(app)/vacations/my/page.tsx @@ -1,6 +1,6 @@ import { MyVacationsClient } from "~/components/vacations/MyVacationsClient.js"; -export const metadata = { title: "My Vacations — Planarchy" }; +export const metadata = { title: "My Vacations — plANARCHY" }; export default function MyVacationsPage() { return ; diff --git a/apps/web/src/app/auth/signin/page.tsx b/apps/web/src/app/auth/signin/page.tsx index f98efa2..3eb9acf 100644 --- a/apps/web/src/app/auth/signin/page.tsx +++ b/apps/web/src/app/auth/signin/page.tsx @@ -37,7 +37,7 @@ export default function SignInPage() {
- Planarchy Control Center + plANARCHY Control Center

Resource planning that stays readable under pressure. @@ -66,7 +66,7 @@ export default function SignInPage() {

Welcome Back

-

Sign in to Planarchy

+

Sign in to plANARCHY

Resource Planning, staffing, and forecasting.

@@ -87,7 +87,7 @@ export default function SignInPage() { value={email} onChange={(e) => setEmail(e.target.value)} className="app-input" - placeholder="admin@planarchy.dev" + placeholder="you@company.com" required />
@@ -116,14 +116,6 @@ export default function SignInPage() { -
-

Demo accounts

-
-

admin@planarchy.dev / admin123

-

manager@planarchy.dev / manager123

-

viewer@planarchy.dev / viewer123

-
-

diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 3a40471..d14eb92 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -16,8 +16,21 @@ const displayFont = Manrope({ }); export const metadata: Metadata = { - title: "Planarchy — Resource Planning", + metadataBase: new URL("https://planarchy.hartmut-noerenberg.com"), + title: "plANARCHY — Resource Planning", description: "Interactive resource planning and project staffing tool", + openGraph: { + title: "plANARCHY — Resource Planning", + description: "Estimates, staffing, chargeability, and timelines in one workspace.", + images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "plANARCHY Logo" }], + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "plANARCHY — Resource Planning", + description: "Estimates, staffing, chargeability, and timelines in one workspace.", + images: ["/og-image.png"], + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/apps/web/src/components/admin/CalculationRulesClient.tsx b/apps/web/src/components/admin/CalculationRulesClient.tsx index 7d7e586..8811190 100644 --- a/apps/web/src/components/admin/CalculationRulesClient.tsx +++ b/apps/web/src/components/admin/CalculationRulesClient.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { trpc } from "~/lib/trpc/client.js"; const TRIGGER_TYPES = ["SICK", "VACATION", "PUBLIC_HOLIDAY", "CUSTOM"] as const; @@ -164,13 +165,27 @@ export function CalculationRulesClient() { - - - - - - - + + + + + + + @@ -233,7 +248,7 @@ export function CalculationRulesClient() {
- + setEditing({ ...editing, name: e.target.value })} @@ -241,7 +256,7 @@ export function CalculationRulesClient() { />
- +
NameTriggerCost EffectChargeabilityScopePriorityActive + Name + + Trigger + + Cost Effect + + Chargeability + + Scope + + Priority + + Active + Actions