@tribe-nest/forge 3.60.0 → 3.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +5 -2
  2. package/src/contexts/CartContext.tsx +34 -1
  3. package/src/contexts/PublicAuthContext.tsx +15 -3
  4. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  5. package/src/data/queries/useAccount.ts +33 -2
  6. package/src/data/queries/useAnalytics.ts +6 -6
  7. package/src/data/queries/useCallChat.ts +2 -2
  8. package/src/data/queries/useChat.ts +10 -1
  9. package/src/data/queries/useCheckouts.ts +2 -1
  10. package/src/data/queries/useCoachingAvailability.ts +10 -1
  11. package/src/data/queries/useCommunities.ts +2 -0
  12. package/src/data/queries/useCommunity.ts +362 -18
  13. package/src/data/queries/useCommunityAgent.ts +247 -0
  14. package/src/data/queries/useCommunityCalendar.ts +150 -0
  15. package/src/data/queries/useCommunityClassroom.ts +75 -0
  16. package/src/data/queries/useCommunityMembers.ts +108 -0
  17. package/src/data/queries/useCommunityModeration.ts +327 -0
  18. package/src/data/queries/useCommunityWiki.ts +137 -0
  19. package/src/data/queries/useCourses.ts +2 -0
  20. package/src/data/queries/useEmailSignIn.ts +79 -0
  21. package/src/data/queries/useEvents.ts +6 -1
  22. package/src/data/queries/useForms.ts +5 -1
  23. package/src/data/queries/useLeadMagnet.ts +6 -2
  24. package/src/data/queries/useMyBookings.ts +24 -1
  25. package/src/data/queries/useMyTickets.ts +24 -1
  26. package/src/data/queries/useNotifications.ts +6 -2
  27. package/src/data/queries/useOrders.ts +2 -1
  28. package/src/data/queries/usePWA.ts +111 -35
  29. package/src/data/queries/useSiteApp.ts +130 -0
  30. package/src/data/queries/useSubscriptions.ts +6 -1
  31. package/src/i18n/de.json +746 -1
  32. package/src/i18n/en.json +746 -1
  33. package/src/index.ts +20 -0
  34. package/src/runtime/pages.spec.ts +5 -0
  35. package/src/runtime/pages.ts +32 -6
  36. package/src/seo/_tests/seo.spec.ts +326 -0
  37. package/src/seo/applySeoToHead.ts +98 -0
  38. package/src/seo/index.ts +9 -0
  39. package/src/seo/resolve.ts +211 -0
  40. package/src/seo/seoHead.ts +61 -0
  41. package/src/seo/siteSeoHead.ts +97 -0
  42. package/src/seo/types.ts +99 -0
  43. package/src/server/_tests/siteBootstrap.spec.ts +65 -0
  44. package/src/server/index.ts +33 -4
  45. package/src/types/models.ts +11 -0
  46. package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
  47. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  48. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  49. package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
  50. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  51. package/src/ui/analytics/useMetaEvents.ts +78 -0
  52. package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
  53. package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
  54. package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
  55. package/src/ui/headless/_tests/communityHarness.tsx +10 -3
  56. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  57. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  58. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  59. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  60. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  61. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  62. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  63. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
  64. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
  65. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  66. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
  67. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  68. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  69. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  70. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  71. package/src/ui/headless/auth/useSignupForm.ts +20 -1
  72. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  73. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  74. package/src/ui/headless/communities/index.ts +41 -0
  75. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  76. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  77. package/src/ui/headless/communities/useCommunityHome.ts +3 -0
  78. package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
  79. package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
  80. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  81. package/src/ui/headless/community/communityPoll.ts +297 -0
  82. package/src/ui/headless/community/countries.ts +123 -0
  83. package/src/ui/headless/community/index.ts +81 -1
  84. package/src/ui/headless/community/linkPreview.ts +71 -0
  85. package/src/ui/headless/community/richLexical.ts +27 -2
  86. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  87. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  88. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  89. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  90. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  91. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  92. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  93. package/src/ui/headless/downloads/index.ts +8 -0
  94. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  95. package/src/ui/headless/index.ts +13 -0
  96. package/src/ui/headless/memberHome/index.ts +10 -0
  97. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  98. package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
  99. package/src/ui/headless/portal/index.ts +57 -0
  100. package/src/ui/headless/portal/usePortal.ts +274 -0
  101. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  102. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  103. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  104. package/src/ui/headless/siteApp/appBar.ts +102 -0
  105. package/src/ui/headless/siteApp/appHome.ts +344 -0
  106. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  107. package/src/ui/headless/siteApp/index.ts +58 -0
  108. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  109. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  110. package/src/ui/headless/work/index.ts +2 -0
  111. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  112. package/src/ui/index.ts +11 -0
  113. package/src/ui/styled/AccountDashboard.tsx +712 -352
  114. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  115. package/src/ui/styled/ChatRoom.tsx +126 -16
  116. package/src/ui/styled/Checkout.tsx +8 -0
  117. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  118. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  119. package/src/ui/styled/CoachingDetail.tsx +2 -0
  120. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  121. package/src/ui/styled/CourseDetail.tsx +2 -0
  122. package/src/ui/styled/DownloadPage.tsx +263 -0
  123. package/src/ui/styled/EventConfirmation.tsx +10 -0
  124. package/src/ui/styled/EventDetail.tsx +10 -0
  125. package/src/ui/styled/FilmLibrary.tsx +22 -9
  126. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  127. package/src/ui/styled/ProductDetail.tsx +12 -0
  128. package/src/ui/styled/PwaRegistration.tsx +5 -1
  129. package/src/ui/styled/SignupForm.tsx +18 -3
  130. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
  131. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  132. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  133. package/src/ui/styled/app/AppBar.tsx +298 -0
  134. package/src/ui/styled/app/AppHome.tsx +1139 -0
  135. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  136. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  137. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  138. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  139. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  140. package/src/ui/styled/app/index.ts +7 -0
  141. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  142. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  143. package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
  144. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  145. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  146. package/src/ui/styled/communities/CommunityHome.tsx +134 -11
  147. package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
  148. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
  149. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  150. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  151. package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
  152. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  153. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  154. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  155. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  156. package/src/ui/styled/communities/index.ts +9 -0
  157. package/src/ui/styled/communities/shared.tsx +39 -11
  158. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  159. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  160. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  161. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  162. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  163. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  164. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  165. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  166. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  167. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  168. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  169. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  170. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  171. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  172. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  173. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  174. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  175. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  176. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  177. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  178. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  179. package/src/ui/styled/community/index.ts +37 -2
  180. package/src/ui/styled/forge-utilities.css +234 -0
  181. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  182. package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
  183. package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
  184. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  185. package/src/ui/styled/members/index.ts +8 -0
  186. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  187. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  188. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  189. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  190. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  191. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  192. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  193. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  194. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  195. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  196. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  197. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  198. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  199. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  200. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  201. package/src/ui/styled/portal/index.ts +13 -0
  202. package/src/ui/styled/portal/portalNav.ts +260 -0
  203. package/src/ui/styled/portal/shared.tsx +206 -0
  204. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  205. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  206. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  207. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  208. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  209. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  210. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  211. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  212. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  213. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  214. package/src/ui/styled/work/index.ts +1 -1
  215. package/src/utils/_tests/landing.spec.ts +39 -0
  216. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  217. package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
  218. package/src/utils/landing.ts +36 -10
  219. package/src/utils/metaEvents.ts +302 -0
  220. package/src/utils/structuredData.ts +339 -1
@@ -1,113 +1,118 @@
1
+ import type { MouseEvent } from "react";
2
+ import { useForgeT, type ForgeT } from "../../../i18n";
3
+ import { internalPath } from "../../../runtime/pages";
1
4
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
2
- import {
3
- formatWorkDate,
4
- formatWorkMoney,
5
- useWorkProjectInvoices,
6
- type WorkProjectInvoice,
7
- } from "../../headless/work/useWorkPortal";
5
+ import { useWorkProjectInvoices, type WorkProjectInvoice } from "../../headless/work/useWorkPortal";
6
+ import { PortalBlock, PortalList, PortalRow, portalMoney, usePortal, usePortalButtons } from "../portal/shared";
8
7
 
9
8
  export interface WorkProjectInvoicesProps {
10
9
  /** The client project id. */
11
10
  projectId: string;
12
11
  /** Build the href for an invoice's pay/detail page. Defaults to
13
- * `/i/invoice/{id}`. */
12
+ * `internalPath("invoice/$invoiceId")`. */
14
13
  invoiceHref?: (invoiceId: string) => string;
14
+ /** Router-driven open handler. Pay and View stay real links (so a new tab
15
+ * still works), and a plain left click calls this instead of reloading. */
16
+ onOpenInvoice?: (invoiceId: string) => void;
15
17
  }
16
18
 
17
- const STATUS_LABEL: Record<string, string> = {
18
- sent: "Awaiting payment",
19
- overdue: "Overdue",
20
- initiated_payment: "Payment started",
21
- payment_failed: "Payment failed",
22
- paid: "Paid",
23
- cancelled: "Cancelled",
19
+ /** Status in words. Literal keys, so the translation spec can see each one. */
20
+ const statusLabel = (tr: ForgeT, status: string): string => {
21
+ const labels: Record<string, string> = {
22
+ sent: tr("forge.work_project_invoices.status_sent"),
23
+ overdue: tr("forge.work_project_invoices.status_overdue"),
24
+ initiated_payment: tr("forge.work_project_invoices.status_initiated_payment"),
25
+ payment_failed: tr("forge.work_project_invoices.status_payment_failed"),
26
+ paid: tr("forge.work_project_invoices.status_paid"),
27
+ cancelled: tr("forge.work_project_invoices.status_cancelled"),
28
+ };
29
+ return labels[status] ?? status.replace(/_/g, " ");
24
30
  };
25
31
 
32
+ /** States the client has to act on. They read in the text colour at weight, never in a warning hue. */
33
+ const needsAttention = (inv: WorkProjectInvoice) => inv.status === "overdue" || inv.status === "payment_failed";
34
+
26
35
  /**
27
36
  * The client's invoices for a project, with a Pay button for anything still
28
37
  * owing (links to the hosted invoice page). Read-only otherwise. Built on
29
38
  * `useWorkProjectInvoices`; theme tokens only. Renders nothing when there are
30
39
  * no invoices yet.
31
40
  */
32
- export function WorkProjectInvoices({ projectId, invoiceHref }: WorkProjectInvoicesProps) {
41
+ export function WorkProjectInvoices({ projectId, invoiceHref, onOpenInvoice }: WorkProjectInvoicesProps) {
33
42
  const t = useThemeTokens();
43
+ const tr = useForgeT();
44
+ const buttons = usePortalButtons();
45
+ const { formatDate } = usePortal();
34
46
  const { data: invoices, isLoading } = useWorkProjectInvoices(projectId);
35
- const href = invoiceHref ?? ((id: string) => `/i/invoice/${id}`);
47
+ const href = invoiceHref ?? ((invoiceId: string) => internalPath("invoice/$invoiceId", { invoiceId }));
36
48
 
37
49
  if (isLoading || !invoices || invoices.length === 0) return null;
38
50
 
39
- const statusColor = (inv: WorkProjectInvoice) =>
40
- inv.status === "paid" ? t.primary : inv.status === "overdue" || inv.status === "payment_failed" ? "#ef4444" : t.muted;
51
+ const open = (invoiceId: string) =>
52
+ onOpenInvoice
53
+ ? (event: MouseEvent<HTMLAnchorElement>) => {
54
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
55
+ event.preventDefault();
56
+ onOpenInvoice(invoiceId);
57
+ }
58
+ : undefined;
41
59
 
42
60
  return (
43
- <div
44
- data-testid="work-project-invoices"
45
- style={{
46
- border: `1px solid ${t.border}`,
47
- borderRadius: t.cornerRadius,
48
- background: t.surface,
49
- padding: 20,
50
- color: t.text,
51
- fontFamily: t.fontFamily,
52
- }}
53
- >
54
- <h2 style={{ fontSize: 16, fontWeight: 800, margin: "0 0 14px" }}>Invoices</h2>
55
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
56
- {invoices.map((inv) => (
57
- <div
58
- key={inv.id}
59
- data-testid={`work-invoice-${inv.id}`}
60
- style={{
61
- display: "flex",
62
- alignItems: "center",
63
- justifyContent: "space-between",
64
- gap: 12,
65
- flexWrap: "wrap",
66
- padding: "12px 14px",
67
- border: `1px solid ${t.border}`,
68
- borderRadius: t.cornerRadius,
69
- }}
70
- >
71
- <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
72
- <span style={{ fontWeight: 700 }}>{inv.invoiceNumber}</span>
73
- <span style={{ fontSize: 12, color: statusColor(inv) }}>
74
- {STATUS_LABEL[inv.status] ?? inv.status}
75
- {inv.status === "paid" && inv.paidAt ? ` · ${formatWorkDate(inv.paidAt)}` : ""}
76
- {inv.payable && inv.dueDate ? ` · due ${formatWorkDate(inv.dueDate)}` : ""}
77
- </span>
78
- </div>
79
- <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
80
- <span style={{ fontWeight: 700 }}>{formatWorkMoney(inv.total, inv.currency)}</span>
81
- {inv.payable ? (
82
- <a
83
- href={href(inv.id)}
84
- data-testid={`work-invoice-pay-${inv.id}`}
85
- style={{
86
- display: "inline-block",
87
- padding: "8px 16px",
88
- borderRadius: t.cornerRadius,
89
- background: t.primary,
90
- color: t.background,
91
- fontWeight: 700,
92
- fontSize: 13,
93
- textDecoration: "none",
94
- }}
95
- >
96
- Pay
97
- </a>
98
- ) : (
99
- <a
100
- href={href(inv.id)}
101
- data-testid={`work-invoice-view-${inv.id}`}
102
- style={{ color: t.primary, fontSize: 13, textDecoration: "underline" }}
103
- >
104
- View
105
- </a>
106
- )}
107
- </div>
108
- </div>
109
- ))}
110
- </div>
61
+ <div data-testid="work-project-invoices" style={{ color: t.text, fontFamily: t.fontFamily }}>
62
+ <PortalBlock title={tr("forge.work_project_invoices.title")}>
63
+ <PortalList>
64
+ {invoices.map((inv) => {
65
+ const attention = needsAttention(inv);
66
+ return (
67
+ <PortalRow key={inv.id} testId={`work-invoice-${inv.id}`}>
68
+ <div className="fg:flex fg:min-w-0 fg:flex-1 fg:flex-col fg:gap-1">
69
+ <span style={{ fontWeight: 600, fontSize: 15 }}>{inv.invoiceNumber}</span>
70
+ <span className="fg:flex fg:flex-wrap fg:gap-x-3" style={{ fontSize: 13, color: t.muted }}>
71
+ <span
72
+ data-testid={`work-invoice-status-${inv.id}`}
73
+ style={attention ? { color: t.text, fontWeight: 600 } : undefined}
74
+ >
75
+ {statusLabel(tr, inv.status)}
76
+ </span>
77
+ {inv.status === "paid" && inv.paidAt ? (
78
+ <span>{tr("forge.work_project_invoices.paid_on", { date: formatDate(inv.paidAt) })}</span>
79
+ ) : null}
80
+ {inv.payable && inv.dueDate ? (
81
+ <span>{tr("forge.work_project_invoices.due_on", { date: formatDate(inv.dueDate) })}</span>
82
+ ) : null}
83
+ </span>
84
+ </div>
85
+ <div className="fg:flex fg:items-center fg:gap-4">
86
+ <span style={{ fontWeight: 600, fontSize: 15, fontVariantNumeric: "tabular-nums" }}>
87
+ {portalMoney(inv.total, inv.currency)}
88
+ </span>
89
+ {inv.payable ? (
90
+ <a
91
+ href={href(inv.id)}
92
+ onClick={open(inv.id)}
93
+ data-testid={`work-invoice-pay-${inv.id}`}
94
+ aria-label={tr("forge.work_project_invoices.pay_aria", { number: inv.invoiceNumber })}
95
+ style={buttons.primary}
96
+ >
97
+ {tr("forge.work_project_invoices.pay")}
98
+ </a>
99
+ ) : (
100
+ <a
101
+ href={href(inv.id)}
102
+ onClick={open(inv.id)}
103
+ data-testid={`work-invoice-view-${inv.id}`}
104
+ aria-label={tr("forge.work_project_invoices.view_aria", { number: inv.invoiceNumber })}
105
+ style={{ ...buttons.quiet, minHeight: 34, padding: "6px 14px" }}
106
+ >
107
+ {tr("forge.work_project_invoices.view")}
108
+ </a>
109
+ )}
110
+ </div>
111
+ </PortalRow>
112
+ );
113
+ })}
114
+ </PortalList>
115
+ </PortalBlock>
111
116
  </div>
112
117
  );
113
118
  }
@@ -1,6 +1,9 @@
1
+ import { useForgeT } from "../../../i18n";
2
+ import { internalPath } from "../../../runtime/pages";
1
3
  import { useThemeTokens } from "../../theme/ForgeThemeProvider";
2
4
  import { Loading } from "../Loading";
3
5
  import { useWorkProjectReport } from "../../headless/work/useWorkPortal";
6
+ import { PortalEmpty } from "../portal/shared";
4
7
  import { WorkReportBody } from "./WorkReportBody";
5
8
  import { WorkProjectInvoices } from "./WorkProjectInvoices";
6
9
  import { WorkProjectDocuments } from "./WorkProjectDocuments";
@@ -9,8 +12,8 @@ export interface WorkProjectReportProps {
9
12
  /** The client project id (from the route). */
10
13
  projectId: string;
11
14
  /** Build the href for a task's detail/comments page. Defaults to
12
- * `/i/work/projects/{projectId}/tasks/{taskId}`. Pass `null` to keep tasks
13
- * read-only (no per-task navigation). */
15
+ * `internalPath("account/projects/$projectId/tasks/$taskId")`. Pass `null`
16
+ * to keep tasks read-only (no per-task navigation). */
14
17
  taskHref?: ((taskId: string) => string) | null;
15
18
  /** Router-driven open handler for a task. When provided, task rows render as
16
19
  * buttons calling this instead of anchors. */
@@ -24,6 +27,7 @@ export interface WorkProjectReportProps {
24
27
  */
25
28
  export function WorkProjectReport({ projectId, taskHref, onOpenTask }: WorkProjectReportProps) {
26
29
  const t = useThemeTokens();
30
+ const tr = useForgeT();
27
31
  const { data: report, isLoading, isError } = useWorkProjectReport(projectId);
28
32
 
29
33
  if (isLoading) {
@@ -32,19 +36,8 @@ export function WorkProjectReport({ projectId, taskHref, onOpenTask }: WorkProje
32
36
 
33
37
  if (isError || !report) {
34
38
  return (
35
- <div
36
- data-testid="work-project-report-error"
37
- style={{
38
- border: `1px solid ${t.border}`,
39
- borderRadius: t.cornerRadius,
40
- background: t.surface,
41
- padding: 32,
42
- textAlign: "center",
43
- color: t.text,
44
- fontFamily: t.fontFamily,
45
- }}
46
- >
47
- <p style={{ margin: 0, color: t.muted }}>We couldn&apos;t find this project.</p>
39
+ <div data-testid="work-project-report-error" style={{ fontFamily: t.fontFamily }}>
40
+ <PortalEmpty title={tr("forge.work_project_report.not_found")} />
48
41
  </div>
49
42
  );
50
43
  }
@@ -54,12 +47,13 @@ export function WorkProjectReport({ projectId, taskHref, onOpenTask }: WorkProje
54
47
  ? undefined
55
48
  : onOpenTask
56
49
  ? undefined
57
- : (taskHref ?? ((taskId: string) => `/i/work/projects/${projectId}/tasks/${taskId}`));
50
+ : (taskHref ??
51
+ ((taskId: string) => internalPath("account/projects/$projectId/tasks/$taskId", { projectId, taskId })));
58
52
 
59
53
  // The documents and invoices sections are authed-only (not part of the
60
54
  // login-free token share, which renders WorkReportBody directly).
61
55
  return (
62
- <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
56
+ <div className="fg:flex fg:flex-col fg:gap-10">
63
57
  <WorkReportBody report={report} taskHref={resolvedTaskHref} onOpenTask={onOpenTask} />
64
58
  <WorkProjectDocuments projectId={projectId} />
65
59
  <WorkProjectInvoices projectId={projectId} />