/)
+ })
+})
diff --git a/apps/admin/src/components/shared/dialog.tsx b/apps/admin/src/components/shared/dialog.tsx
index 89745f3..1719ad3 100644
--- a/apps/admin/src/components/shared/dialog.tsx
+++ b/apps/admin/src/components/shared/dialog.tsx
@@ -13,6 +13,8 @@ interface ConfirmRequest {
confirmLabel?: string
cancelLabel?: string
danger?: boolean
+ details?: string[]
+ acknowledgeOnly?: boolean
resolve: (ok: boolean) => void
}
@@ -25,6 +27,7 @@ interface PromptRequest {
defaultValue?: string
confirmLabel?: string
required?: boolean
+ maxLength?: number
danger?: boolean
resolve: (value: string | null) => void
}
@@ -121,6 +124,15 @@ export function DialogHost() {
{current.body &&
{current.body}
}
+ {current.kind === "confirm" && current.details && current.details.length > 0 && (
+
+ {current.details.map((line) => (
+ -
+ {line}
+
+ ))}
+
+ )}
{current.kind === "prompt" && (
<>
{current.label &&
}
@@ -130,15 +142,18 @@ export function DialogHost() {
rows={3}
value={value}
placeholder={current.placeholder}
+ maxLength={current.maxLength}
onChange={(e) => setValue(e.target.value)}
/>
>
)}
-
+ {!(current.kind === "confirm" && current.acknowledgeOnly) && (
+
+ )}
)
})}
-
-
-
+ {tab === "hours" ? (
+
+ ) : (
+
+
+
+ )}
{deleted && (
diff --git a/apps/admin/src/lib/query.ts b/apps/admin/src/lib/query.ts
index b864508..0478538 100644
--- a/apps/admin/src/lib/query.ts
+++ b/apps/admin/src/lib/query.ts
@@ -73,6 +73,8 @@ export const queryKeys = {
events: {
all: ["admin", "events"] as const,
list: (params?: unknown) => ["admin", "events", "list", params ?? null] as const,
+ /** One flat page (home preview, event picker) — kept apart from the infinite `list` cache entries. */
+ page: (params?: unknown) => ["admin", "events", "page", params ?? null] as const,
detail: (id: string) => ["admin", "events", "detail", id] as const,
},
@@ -86,6 +88,7 @@ export const queryKeys = {
events: (id: string, params?: unknown) => ["admin", "users", id, "events", params ?? null] as const,
messages: (id: string, params?: unknown) =>
["admin", "users", id, "messages", params ?? null] as const,
+ hours: (id: string, params?: unknown) => ["admin", "users", id, "hours", params ?? null] as const,
},
mail: {
diff --git a/apps/admin/src/styles/app.css b/apps/admin/src/styles/app.css
index ee278e6..51e0873 100644
--- a/apps/admin/src/styles/app.css
+++ b/apps/admin/src/styles/app.css
@@ -587,6 +587,42 @@ img.org-logo { object-fit: cover; background: var(--paper-2); }
}
.user-org-row .title.lnk-inline { cursor: pointer; }
+/* Users page, Hours tab: the ledger totals strip under the tabs, and one .prow per ledger entry. */
+.user-detail .user-hours-stats { margin-top: var(--space-3); margin-bottom: 0; }
+.prow-hours {
+ flex-shrink: 0;
+ font-family: var(--font-mono);
+ font-size: var(--fs-13);
+ font-weight: 700;
+ color: var(--ink);
+ font-feature-settings: "tnum" 1;
+ white-space: nowrap;
+}
+.prow-meta.prow-note { white-space: normal; font-style: italic; }
+.prow.ledger-row.removed .prow-main { opacity: 0.6; }
+.prow.ledger-row.removed .prow-hours { text-decoration: line-through; }
+.user-hours-head {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--space-2);
+ margin: var(--space-3) var(--space-4) 0;
+}
+.user-hours-head .hint { font-size: var(--fs-12); color: var(--ink-3); }
+.sub.user-hours-credit { margin: var(--space-3) var(--space-4) 0; }
+.user-hours-credit .seg { align-self: flex-start; }
+.user-hours-credit .user-hours-amount, .user-hours-credit .user-hours-date { max-width: 180px; }
+
+.dialog-list {
+ margin: 0;
+ padding-left: var(--space-5);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ font-size: var(--fs-12);
+ color: var(--ink-2);
+}
+
/* Member row actions menu. */
.member-row .title.lnk-inline { cursor: pointer; }
.btn.icon { width: 28px; padding: 0; justify-content: center; }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c961fb..c641e6a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,8 +25,8 @@ importers:
apps/admin:
dependencies:
'@civfix/shared':
- specifier: ^0.57.0
- version: 0.57.0
+ specifier: 0.58.0-b-feat-admin-user-hours-contract.g47d8a38
+ version: 0.58.0-b-feat-admin-user-hours-contract.g47d8a38
'@tanstack/react-query':
specifier: ^5.62.7
version: 5.101.0(react@19.0.0)
@@ -101,8 +101,8 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@civfix/shared@0.57.0':
- resolution: {integrity: sha512-v4IFb4pR9hbnf+AueRmL7GLB8DIhG9c9ZJjAnXKK3Yqjsv0Mbmhj0Khmb27YRgKL1Aoe//7nLiFkgj11Qrg7GQ==}
+ '@civfix/shared@0.58.0-b-feat-admin-user-hours-contract.g47d8a38':
+ resolution: {integrity: sha512-J2l/KZ19Nn98wV8iS1EF3lUmCUUTdElMxnRH3bO+ij/RhGMo02O35u22Ly8nvt7rMC0qf4DNH2BZo9z23l5tLQ==}
engines: {node: '>=20'}
'@emnapi/core@1.10.0':
@@ -2484,7 +2484,7 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@civfix/shared@0.57.0':
+ '@civfix/shared@0.58.0-b-feat-admin-user-hours-contract.g47d8a38':
dependencies:
zod: 3.25.76