|
|
@@ -1,5 +1,5 @@
|
|
|
import { redirect } from 'next/navigation'
|
|
|
-import { getTranslations } from 'next-intl/server'
|
|
|
+import { getLocale, getTranslations } from 'next-intl/server'
|
|
|
import { connectorName } from '@/features/integrations/Lib/display-name'
|
|
|
import { NoAccess } from './no-access'
|
|
|
import { AppSidebar } from '@/components/app-sidebar'
|
|
|
@@ -58,6 +58,9 @@ import { TimeClockProvider } from '@/features/time-tracking/Components/TimeClock
|
|
|
import { RealtimeProvider } from '@/features/realtime/RealtimeProvider'
|
|
|
import { SaveShortcutListener } from '@/components/save-shortcut-listener'
|
|
|
import { AnalyticsIdentity } from '@/components/analytics-identity'
|
|
|
+import { WhatsNewProvider } from '@/features/whats-new/Components/WhatsNewProvider'
|
|
|
+import { getWhatsNewFeed } from '@/features/whats-new/Lib/feed.server'
|
|
|
+import { selectEntries, unreadIds } from '@/features/whats-new/Lib/select'
|
|
|
|
|
|
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
|
const data = await getLayoutData()
|
|
|
@@ -173,6 +176,25 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|
|
return <NoAccess organizationName={org?.name ?? ''} />
|
|
|
}
|
|
|
|
|
|
+ // The highlights written on torqvoice.com, narrowed to what this person can
|
|
|
+ // actually open on this install. A licensed self-hosted install runs under
|
|
|
+ // the workshop's own name, and news signed by Torqvoice has no place there.
|
|
|
+ const appVersion = process.env.APP_VERSION || 'development'
|
|
|
+ const releaseNotesUrl =
|
|
|
+ process.env.RELEASE_NOTES_URL || 'https://github.com/Torqvoice/torqvoice/releases'
|
|
|
+ const whatsNewHidden = !isCloudMode() && features.brandingRemoved
|
|
|
+ const whatsNewEntries = whatsNewHidden
|
|
|
+ ? []
|
|
|
+ : selectEntries(await getWhatsNewFeed(await getLocale()), {
|
|
|
+ appVersion,
|
|
|
+ cloud: isCloudMode(),
|
|
|
+ visibleSubjects,
|
|
|
+ })
|
|
|
+ const whatsNewUnread = unreadIds(whatsNewEntries, {
|
|
|
+ seenAt: data.whatsNewSeenAt,
|
|
|
+ accountCreatedAt: data.userCreatedAt,
|
|
|
+ })
|
|
|
+
|
|
|
// Live work for the sidebar pills. Each is a count of things still in hand,
|
|
|
// never a running total, so the numbers stay small enough to mean
|
|
|
// something. Only the screens this role can see are counted at all.
|
|
|
@@ -309,130 +331,136 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|
|
dismissed={licenseExpiryDismissed}
|
|
|
>
|
|
|
<WhiteLabelCtaProvider show={showWhiteLabelCta}>
|
|
|
- {/* Accent line along the very top of the viewport — the card hairline at
|
|
|
+ <WhatsNewProvider
|
|
|
+ entries={whatsNewEntries}
|
|
|
+ unreadIds={whatsNewUnread}
|
|
|
+ timeZone={safeTimeZone(data.timezone)}
|
|
|
+ appVersion={appVersion}
|
|
|
+ releaseNotesUrl={releaseNotesUrl}
|
|
|
+ feedback={isDemoMode ? null : isCloudMode() ? 'identified' : 'anonymous'}
|
|
|
+ >
|
|
|
+ {/* Accent line along the very top of the viewport — the card hairline at
|
|
|
page scale: primary on the left, gone by the far edge. Marks where the
|
|
|
app begins against the browser chrome. */}
|
|
|
- <div
|
|
|
- aria-hidden
|
|
|
- className="pointer-events-none fixed inset-x-0 top-0 z-50 h-px bg-linear-to-r from-primary via-primary/35 to-transparent"
|
|
|
- />
|
|
|
- {/* Watches for a notice posted while this page is already open. Here
|
|
|
+ <div
|
|
|
+ aria-hidden
|
|
|
+ className="pointer-events-none fixed inset-x-0 top-0 z-50 h-px bg-linear-to-r from-primary via-primary/35 to-transparent"
|
|
|
+ />
|
|
|
+ {/* Watches for a notice posted while this page is already open. Here
|
|
|
rather than beside the banner because the socket authenticates on
|
|
|
the session cookie, which the sign-in page does not have. */}
|
|
|
- <BroadcastLive />
|
|
|
- {/* The demo banner already occupies the header, and demo image tags
|
|
|
+ <BroadcastLive />
|
|
|
+ {/* The demo banner already occupies the header, and demo image tags
|
|
|
(demo-abc1234) are not versions a visitor should be notified about. */}
|
|
|
- {!isDemoMode && (
|
|
|
- <UpdateBanner
|
|
|
- currentVersion={process.env.APP_VERSION || 'development'}
|
|
|
- lastSeenVersion={data.lastSeenVersion}
|
|
|
- shownVersion={data.updateBannerVersion}
|
|
|
- shownAt={data.updateBannerShownAt?.toISOString() ?? null}
|
|
|
- releaseNotesUrl={
|
|
|
- process.env.RELEASE_NOTES_URL ||
|
|
|
- 'https://github.com/Torqvoice/torqvoice/releases'
|
|
|
+ {!isDemoMode && (
|
|
|
+ <UpdateBanner
|
|
|
+ currentVersion={appVersion}
|
|
|
+ lastSeenVersion={data.lastSeenVersion}
|
|
|
+ shownVersion={data.updateBannerVersion}
|
|
|
+ shownAt={data.updateBannerShownAt?.toISOString() ?? null}
|
|
|
+ releaseNotesUrl={releaseNotesUrl}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <SidebarProvider
|
|
|
+ defaultOpen={sidebarOpen}
|
|
|
+ style={
|
|
|
+ {
|
|
|
+ '--sidebar-width': '19rem',
|
|
|
+ } as React.CSSProperties
|
|
|
}
|
|
|
- />
|
|
|
- )}
|
|
|
- <SidebarProvider
|
|
|
- defaultOpen={sidebarOpen}
|
|
|
- style={
|
|
|
- {
|
|
|
- '--sidebar-width': '19rem',
|
|
|
- } as React.CSSProperties
|
|
|
- }
|
|
|
- >
|
|
|
- <DateSettingsProvider
|
|
|
- dateFormat={data.dateFormat}
|
|
|
- timeFormat={data.timeFormat}
|
|
|
- timezone={data.timezone}
|
|
|
- weekStartDay={data.weekStartDay}
|
|
|
>
|
|
|
- <CurrencySettingsProvider
|
|
|
- currencyCode={data.currencyCode}
|
|
|
- currencyFormat={data.currencyFormat}
|
|
|
+ <DateSettingsProvider
|
|
|
+ dateFormat={data.dateFormat}
|
|
|
+ timeFormat={data.timeFormat}
|
|
|
+ timezone={data.timezone}
|
|
|
+ weekStartDay={data.weekStartDay}
|
|
|
>
|
|
|
- <ConfirmProvider>
|
|
|
- <PlateLookupProvider
|
|
|
- value={{
|
|
|
- available: plateLookupAvailable,
|
|
|
- canCreate: canCreateVehicles,
|
|
|
- registryName: plateLookupRegistry,
|
|
|
- }}
|
|
|
- >
|
|
|
- <FeatureHintProvider
|
|
|
- initialSeen={seenHints}
|
|
|
- pending={[...pendingHints, ...announcements]}
|
|
|
+ <CurrencySettingsProvider
|
|
|
+ currencyCode={data.currencyCode}
|
|
|
+ currencyFormat={data.currencyFormat}
|
|
|
+ >
|
|
|
+ <ConfirmProvider>
|
|
|
+ <PlateLookupProvider
|
|
|
+ value={{
|
|
|
+ available: plateLookupAvailable,
|
|
|
+ canCreate: canCreateVehicles,
|
|
|
+ registryName: plateLookupRegistry,
|
|
|
+ }}
|
|
|
>
|
|
|
- {/* One socket for the whole app: record changes, who
|
|
|
+ <FeatureHintProvider
|
|
|
+ initialSeen={seenHints}
|
|
|
+ pending={[...pendingHints, ...announcements]}
|
|
|
+ >
|
|
|
+ {/* One socket for the whole app: record changes, who
|
|
|
else is on a record, and the older workshop-wide
|
|
|
channels the remaining pages still read. */}
|
|
|
- <RealtimeProvider>
|
|
|
- <TimeClockProvider technicianIds={technicianIds}>
|
|
|
- <SaveShortcutListener />
|
|
|
- <AnalyticsIdentity
|
|
|
- userId={data.userId}
|
|
|
- organizationId={data.organizationId}
|
|
|
- />
|
|
|
- <AppSidebar
|
|
|
- companyLogo={data.companyLogo}
|
|
|
- organizations={data.organizations}
|
|
|
- activeOrgId={data.organizationId}
|
|
|
- isSuperAdmin={data.isSuperAdmin}
|
|
|
- features={features}
|
|
|
- tireHotelEnabled={tireHotelEnabled}
|
|
|
- aiEnabled={aiEnabled}
|
|
|
- faultCodesEnabled={faultCodesEnabled}
|
|
|
- visibleSubjects={visibleSubjects}
|
|
|
- announcement={announcements[0] ?? null}
|
|
|
- isAdminOrOwner={isOwnerOrAdmin}
|
|
|
- counts={sidebarCounts}
|
|
|
- isTechnician={technicianIds.length > 0}
|
|
|
- />
|
|
|
- <SidebarInset>
|
|
|
- {/* A flex column with a real height, so the `flex-1` every
|
|
|
+ <RealtimeProvider>
|
|
|
+ <TimeClockProvider technicianIds={technicianIds}>
|
|
|
+ <SaveShortcutListener />
|
|
|
+ <AnalyticsIdentity
|
|
|
+ userId={data.userId}
|
|
|
+ organizationId={data.organizationId}
|
|
|
+ />
|
|
|
+ <AppSidebar
|
|
|
+ companyLogo={data.companyLogo}
|
|
|
+ organizations={data.organizations}
|
|
|
+ activeOrgId={data.organizationId}
|
|
|
+ isSuperAdmin={data.isSuperAdmin}
|
|
|
+ features={features}
|
|
|
+ tireHotelEnabled={tireHotelEnabled}
|
|
|
+ aiEnabled={aiEnabled}
|
|
|
+ faultCodesEnabled={faultCodesEnabled}
|
|
|
+ visibleSubjects={visibleSubjects}
|
|
|
+ announcement={announcements[0] ?? null}
|
|
|
+ isAdminOrOwner={isOwnerOrAdmin}
|
|
|
+ counts={sidebarCounts}
|
|
|
+ isTechnician={technicianIds.length > 0}
|
|
|
+ />
|
|
|
+ <SidebarInset>
|
|
|
+ {/* A flex column with a real height, so the `flex-1` every
|
|
|
page already writes on its wrapper actually resolves.
|
|
|
Without it a page that wants to fill the window (the
|
|
|
work board's week timeline) stopped at its content and
|
|
|
left the rest of the screen blank. */}
|
|
|
- <div className="flex min-h-0 flex-1 flex-col pb-14 md:pb-0">
|
|
|
- {children}
|
|
|
- </div>
|
|
|
- </SidebarInset>
|
|
|
- <SearchCommand
|
|
|
- visibleSubjects={visibleSubjects}
|
|
|
- features={features}
|
|
|
- tireHotelEnabled={tireHotelEnabled}
|
|
|
- aiEnabled={aiEnabled}
|
|
|
- isAdminOrOwner={isOwnerOrAdmin}
|
|
|
- isCloud={cloudLinked}
|
|
|
- supportEnabled={supportEnabled}
|
|
|
- isMarine={data.serviceType === 'marine'}
|
|
|
- connectors={searchConnectors}
|
|
|
- faultCodesEnabled={faultCodesEnabled}
|
|
|
- />
|
|
|
- <PlateLookupCommand />
|
|
|
- {isOwnerOrAdmin && <NotificationInitializer />}
|
|
|
- <OnlineTracker />
|
|
|
- <ConnectionWatcher />
|
|
|
- <UpdateWatcher version={buildVersion()} />
|
|
|
- <InstallBanner
|
|
|
- appName={appBranding.name}
|
|
|
- iconUrl={appBranding.iconUrl}
|
|
|
- />
|
|
|
- </TimeClockProvider>
|
|
|
- </RealtimeProvider>
|
|
|
- </FeatureHintProvider>
|
|
|
- </PlateLookupProvider>
|
|
|
- </ConfirmProvider>
|
|
|
- </CurrencySettingsProvider>
|
|
|
- </DateSettingsProvider>
|
|
|
- </SidebarProvider>
|
|
|
- <MobileBottomNav
|
|
|
- isSuperAdmin={data.isSuperAdmin}
|
|
|
- tireHotelEnabled={tireHotelEnabled}
|
|
|
- />
|
|
|
- {supportEnabled && <SupportBubble userId={data.userId} />}
|
|
|
+ <div className="flex min-h-0 flex-1 flex-col pb-14 md:pb-0">
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
+ </SidebarInset>
|
|
|
+ <SearchCommand
|
|
|
+ visibleSubjects={visibleSubjects}
|
|
|
+ features={features}
|
|
|
+ tireHotelEnabled={tireHotelEnabled}
|
|
|
+ aiEnabled={aiEnabled}
|
|
|
+ isAdminOrOwner={isOwnerOrAdmin}
|
|
|
+ isCloud={cloudLinked}
|
|
|
+ supportEnabled={supportEnabled}
|
|
|
+ isMarine={data.serviceType === 'marine'}
|
|
|
+ connectors={searchConnectors}
|
|
|
+ faultCodesEnabled={faultCodesEnabled}
|
|
|
+ />
|
|
|
+ <PlateLookupCommand />
|
|
|
+ {isOwnerOrAdmin && <NotificationInitializer />}
|
|
|
+ <OnlineTracker />
|
|
|
+ <ConnectionWatcher />
|
|
|
+ <UpdateWatcher version={buildVersion()} />
|
|
|
+ <InstallBanner
|
|
|
+ appName={appBranding.name}
|
|
|
+ iconUrl={appBranding.iconUrl}
|
|
|
+ />
|
|
|
+ </TimeClockProvider>
|
|
|
+ </RealtimeProvider>
|
|
|
+ </FeatureHintProvider>
|
|
|
+ </PlateLookupProvider>
|
|
|
+ </ConfirmProvider>
|
|
|
+ </CurrencySettingsProvider>
|
|
|
+ </DateSettingsProvider>
|
|
|
+ </SidebarProvider>
|
|
|
+ <MobileBottomNav
|
|
|
+ isSuperAdmin={data.isSuperAdmin}
|
|
|
+ tireHotelEnabled={tireHotelEnabled}
|
|
|
+ />
|
|
|
+ {supportEnabled && <SupportBubble userId={data.userId} />}
|
|
|
+ </WhatsNewProvider>
|
|
|
</WhiteLabelCtaProvider>
|
|
|
</LicenseExpiryProvider>
|
|
|
</EngineSizeUnitProvider>
|