feat(smartsheet): cycle 7 — workbook hub

Replaces two independent Smartsheet configs (water log + time tracking)
with one brand-level workbook connection that owns the encrypted API
token. Per-feature configs keep their sheet_id + column_mapping +
frequency + sync_enabled.

Schema (migration 0096):
- NEW smartsheet_workspace: one row per brand. Holds encrypted_api_token
  + token_iv + token_auth_tag (AES-256-GCM, same key), default_sheet_id,
  connection_enabled (master switch), last_test_*, audit columns. RLS
  brand-scoped.
- Backfill: water_smartsheet_config rows copy into workspace first;
  time_tracking_smartsheet_config fills in only brands with no workspace
  row yet. Idempotent (NOT EXISTS + ON CONFLICT DO NOTHING).
- DROP encrypted_api_token + token_iv + token_auth_tag from both feature
  configs (destructive — bytes are copied to workspace first).
- Drop unused bytea customType from both schemas (no longer needed).

Actions:
- NEW src/actions/smartsheet/workspace.ts: getSmartsheetWorkspace,
  saveSmartsheetWorkspace, testSmartsheetWorkspaceConnection,
  disableSmartsheetWorkspace. Permission: can_manage_settings +
  platform_admin.
- NEW src/services/workspace-token.ts (server-only): resolveWorkspaceToken
  helper. Lives outside the 'use server' file so the plaintext token
  is not exposed as an RPC surface — only callable from server-side
  sync engines.
- MODIFIED water-log + time-tracking smartsheet actions: refactored to
  read token via resolveWorkspaceToken; save* no longer accepts a
  token; test* falls through to the workspace token if no token given.
- MODIFIED both sync services: load token from resolveWorkspaceToken
  instead of decrypting the per-feature config. 'connection_disabled'
  now skips cleanly (no retry, no failed log row) — pausing the
  workspace at the hub level no longer floods Recent Activity with
  'disabled' failures.

UI:
- NEW src/components/admin/water-log/SmartsheetHub.tsx: top-level hub
  card. Owns the token + Test Connection + connection enabled toggle +
  default sheet ID + last-verified badge. Permission gated.
- MODIFIED SmartsheetIntegrationCard.tsx (water): rewritten without
  token UI. Reads masked token from workspace. Sheet ID + column
  mapping + frequency + enabled + backfill + recent activity.
- MODIFIED TimeTrackingSmartsheetCard.tsx: same treatment.
- MODIFIED /admin/water-log/settings/page: collapses §05 + §06 into a
  single Smartsheet section with the hub on top + both feature cards
  as siblings underneath.

Tuxedo-only throughout (TUXEDO_BRAND_ID hardcoded in the settings
page; matches existing single-tenant /water convention).

Security fixes from pr-reviewer:
- resolveWorkspaceToken extracted from 'use server' file to a
  server-only service module — was reachable as an RPC, would have
  returned the plaintext token to any authenticated admin.
- getSmartsheetWorkspace now actually checks the auth result instead
  of calling requireManageSettings() and discarding the return value.

Diff: -1742 / +597 lines (net -1100 from the simpler per-feature card).
This commit is contained in:
RouteComm Dev
2026-07-03 19:18:49 -06:00
parent 089d77aa68
commit ad4d3c9976
14 changed files with 1722 additions and 1764 deletions
@@ -4,11 +4,10 @@
* TimeTrackingSmartsheetCard — admin `/admin/water-log/settings`
* card for the Time Tracking → Smartsheet integration (Cycle 5).
*
* Slimmer than the water-log SmartsheetIntegrationCard because the
* user explicitly wants this config-deferred they have no sheet
* ID yet. When they drop one in, they'll get the full column-
* mapping form (Test Connection gates it). Until then, only Sheet
* ID / token / frequency / sync-enabled controls are visible.
* Cycle 7: this card no longer owns the API token. The token lives
* on the workspace (one per brand, owned by the hub card above).
* This card only manages the per-feature mapping: sheet ID + column
* mapping + frequency + sync enabled + recent activity.
*
* Self-contained client component. Takes `brandId` as a prop per
* the brand-isolation convention.
@@ -46,8 +45,6 @@ type Status =
type State = {
config: TTSmartsheetConfigResponse | null;
sheetIdInput: string;
tokenInput: string;
showTokenInput: boolean;
frequency: TTSmartsheetFrequency;
syncEnabled: boolean;
columns: TTColumnOption[];
@@ -69,8 +66,6 @@ type TTColumnOption = {
type Action =
| { type: "SET_CONFIG"; config: TTSmartsheetConfigResponse }
| { type: "SET_SHEET_ID"; value: string }
| { type: "SET_TOKEN"; value: string }
| { type: "SET_SHOW_TOKEN"; value: boolean }
| { type: "SET_FREQUENCY"; value: TTSmartsheetFrequency }
| { type: "SET_SYNC_ENABLED"; value: boolean }
| { type: "SET_COLUMNS"; columns: NonNullable<State["columns"]> }
@@ -103,17 +98,12 @@ function reducer(state: State, action: Action): State {
sheetIdInput: cfg.sheetId ?? "",
frequency: cfg.syncFrequency,
syncEnabled: cfg.syncEnabled,
showTokenInput: !cfg.hasToken,
columnMapping: cfg.columnMapping ?? { ...EMPTY_MAPPING },
loading: false,
};
}
case "SET_SHEET_ID":
return { ...state, sheetIdInput: action.value };
case "SET_TOKEN":
return { ...state, tokenInput: action.value };
case "SET_SHOW_TOKEN":
return { ...state, showTokenInput: action.value };
case "SET_FREQUENCY":
return { ...state, frequency: action.value };
case "SET_SYNC_ENABLED":
@@ -147,8 +137,6 @@ function initialState(): State {
return {
config: null,
sheetIdInput: "",
tokenInput: "",
showTokenInput: true,
frequency: "hourly",
syncEnabled: false,
columns: [],
@@ -181,12 +169,17 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
void refresh();
}, [refresh]);
// Cycle 7: Test Connection uses the workspace's saved token (no
// per-feature token to pass). The workspace action
// `testSmartsheetWorkspaceConnection` is the hub's button; here we
// call the per-feature test action with no token, which falls
// through to the workspace resolver.
const onTestConnection = useCallback(async () => {
dispatch({ type: "SET_TESTING", value: true });
dispatch({ type: "SET_STATUS", value: { kind: "idle" } });
const result = await testTTSmartsheetConnection(brandId, {
sheetId: state.sheetIdInput,
token: state.tokenInput || undefined,
// No `token` — the action pulls the saved workspace token.
});
if (!result.success) {
dispatch({
@@ -200,14 +193,16 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
dispatch({ type: "SET_STATUS", value: { kind: "idle" } });
dispatch({ type: "SET_TESTING", value: false });
force({});
}, [brandId, state.sheetIdInput, state.tokenInput]);
}, [brandId, state.sheetIdInput]);
const onSave = useCallback(async () => {
dispatch({ type: "SET_SAVING", value: true });
dispatch({ type: "SET_STATUS", value: { kind: "idle" } });
const result = await saveTTSmartsheetConfig(brandId, {
sheetId: state.sheetIdInput,
token: state.tokenInput,
// Cycle 7: token is owned by the workspace; this field is
// ignored on save but kept for type compat.
token: "",
syncEnabled: state.syncEnabled,
syncFrequency: state.frequency,
columnMapping: state.columnMapping,
@@ -251,6 +246,9 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
);
}
const hasWorkspaceToken = state.config?.hasToken === true;
const maskedToken = state.config?.maskedToken ?? null;
return (
<div
className="rounded-2xl border border-[#e6dfd1] bg-white p-6 shadow-sm"
@@ -259,12 +257,12 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-xl font-bold text-[#1a4d2e]">
Smartsheet (Time Tracking)
Time Tracking Smartsheet
</h2>
<p className="mt-1 text-sm text-[rgba(60,60,67,0.65)]">
Push every closed clock-out to a configured Smartsheet sheet.
Same encryption + retry policy as the Water Log smartsheet
integration.
Push every closed clock-out to a sheet inside the connected
Smartsheet workbook. The API token is managed by the hub
card above; this card only configures the sheet mapping.
</p>
</div>
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-[#1a4d2e]">
@@ -284,8 +282,24 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
</label>
</div>
{/* Workspace token reference */}
<div className="mb-4 rounded-md border border-[#e6dfd1] bg-[#fdfaf2] px-3 py-2 text-xs text-[rgba(60,60,67,0.65)]">
{hasWorkspaceToken && maskedToken ? (
<>
Using workbook token{" "}
<span className="font-mono text-[#1a4d2e]">{maskedToken}</span>{" "}
from the hub card above.
</>
) : (
<>
No Smartsheet workbook is connected yet. Save the hub card
above with an API token before configuring this sheet.
</>
)}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<div className="sm:col-span-2">
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-[rgba(60,60,67,0.65)]">
Sheet ID or URL
</label>
@@ -299,44 +313,9 @@ export function TimeTrackingSmartsheetCard({ brandId }: Props) {
className="w-full rounded-md border border-[#e6dfd1] bg-[#fdfaf2] px-3 py-2 text-sm"
data-test="tt-smartsheet-sheet-id"
/>
</div>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-[rgba(60,60,67,0.65)]">
API token
</label>
<div className="flex items-center gap-2">
<input
type={state.showTokenInput ? "text" : "password"}
placeholder={
state.config?.hasToken && !state.showTokenInput
? state.config.maskedToken ?? "••••••••"
: "paste new token here"
}
value={state.tokenInput}
onChange={(e) =>
dispatch({ type: "SET_TOKEN", value: e.target.value })
}
className="w-full rounded-md border border-[#e6dfd1] bg-[#fdfaf2] px-3 py-2 text-sm"
data-test="tt-smartsheet-token"
/>
<button
type="button"
className="rounded-md border border-[#e6dfd1] bg-white px-2 py-1 text-xs font-semibold text-[rgba(60,60,67,0.65)] hover:bg-[#fdfaf2]"
onClick={() =>
dispatch({
type: "SET_SHOW_TOKEN",
value: !state.showTokenInput,
})
}
>
{state.showTokenInput ? "Hide" : "Show"}
</button>
</div>
{state.config?.hasToken && !state.tokenInput && (
<p className="mt-1 text-xs text-[rgba(60,60,67,0.55)]">
A token is saved. Leave blank to keep it.
</p>
)}
<p className="mt-1 text-xs text-[rgba(60,60,67,0.55)]">
Pick the sheet inside the workbook where clock-outs should land.
</p>
</div>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-[rgba(60,60,67,0.65)]">
@@ -549,4 +528,4 @@ function RecentActivity({
);
}
export default TimeTrackingSmartsheetCard;
export default TimeTrackingSmartsheetCard;
@@ -0,0 +1,455 @@
"use client";
/**
* SmartsheetHub — top-level card for `/admin/water-log/settings`.
*
* Cycle 7. Owns the brand-level Smartsheet workbook connection:
* - API token (encrypted at rest in `smartsheet_workspace`)
* - "Test Connection" button — verifies the token against a sheet
* - Connection enable toggle (master kill-switch)
* - Default sheet ID (the "home" tab in the workbook)
* - Last-verified badge
*
* Per-feature sheet mappings (water log + time tracking) live in their
* own cards BELOW this one and inherit the workspace token.
*
* Visual language matches the parent Water Log settings page
* (cream + forest palette, "Field Almanac" style).
*/
import { useCallback, useEffect, useReducer, useState } from "react";
import {
getSmartsheetWorkspace,
saveSmartsheetWorkspace,
testSmartsheetWorkspaceConnection,
disableSmartsheetWorkspace,
} from "@/actions/smartsheet/workspace";
import { formatDateTime } from "@/lib/format-date";
// ── State ──────────────────────────────────────────────────────────────────
type State = {
loading: boolean;
saving: boolean;
message: { type: "success" | "error"; text: string } | null;
workspace: Awaited<ReturnType<typeof getSmartsheetWorkspace>> | null;
// Editable form fields:
tokenInput: string;
defaultSheetIdInput: string;
showToken: boolean;
connectionEnabled: boolean;
};
type Action =
| { type: "load"; workspace: State["workspace"] }
| { type: "loading"; loading: boolean }
| { type: "saving"; saving: boolean }
| { type: "token"; value: string }
| { type: "showToken"; value: boolean }
| { type: "defaultSheet"; value: string }
| { type: "connection"; value: boolean }
| { type: "message"; message: State["message"] }
| { type: "reset-message" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "load":
return {
...state,
workspace: action.workspace,
connectionEnabled: action.workspace?.connectionEnabled ?? true,
defaultSheetIdInput: action.workspace?.defaultSheetId ?? "",
};
case "loading":
return { ...state, loading: action.loading };
case "saving":
return { ...state, saving: action.saving };
case "token":
return { ...state, tokenInput: action.value };
case "showToken":
return { ...state, showToken: action.value };
case "defaultSheet":
return { ...state, defaultSheetIdInput: action.value };
case "connection":
return { ...state, connectionEnabled: action.value };
case "message":
return { ...state, message: action.message };
case "reset-message":
return { ...state, message: null };
}
}
const INITIAL_STATE: State = {
loading: true,
saving: false,
message: null,
workspace: null,
tokenInput: "",
defaultSheetIdInput: "",
showToken: false,
connectionEnabled: true,
};
// ── Component ─────────────────────────────────────────────────────────────
export function SmartsheetHub({ brandId }: { brandId: string }) {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
const [testResult, setTestResult] = useState<
| { state: "idle" }
| { state: "loading" }
| { state: "ok"; sheetName: string; columnCount: number }
| { state: "error"; message: string }
>({ state: "idle" });
// ── Load on mount + brandId change ──
useEffect(() => {
let cancelled = false;
void (async () => {
dispatch({ type: "loading", loading: true });
try {
const ws = await getSmartsheetWorkspace(brandId);
if (!cancelled) {
dispatch({ type: "load", workspace: ws });
dispatch({ type: "loading", loading: false });
}
} catch (err) {
if (!cancelled) {
dispatch({
type: "message",
message: {
type: "error",
text: err instanceof Error ? err.message : "Failed to load workspace",
},
});
dispatch({ type: "loading", loading: false });
}
}
})();
return () => {
cancelled = true;
};
}, [brandId]);
// ── Save ──
const handleSave = useCallback(async () => {
dispatch({ type: "saving", saving: true });
dispatch({ type: "reset-message" });
try {
const defaultSheetId =
state.defaultSheetIdInput.trim().length > 0
? state.defaultSheetIdInput.trim()
: null;
const result = await saveSmartsheetWorkspace(brandId, {
token: state.tokenInput,
defaultSheetId,
connectionEnabled: state.connectionEnabled,
});
if (!result.success) {
dispatch({
type: "message",
message: { type: "error", text: result.error },
});
return;
}
dispatch({ type: "load", workspace: result.workspace });
// Clear the token input on success — never leave it in the form.
dispatch({ type: "token", value: "" });
dispatch({ type: "showToken", value: false });
dispatch({
type: "message",
message: {
type: "success",
text: state.tokenInput.length > 0
? "Workspace connected."
: "Workspace settings saved.",
},
});
} catch (err) {
dispatch({
type: "message",
message: {
type: "error",
text: err instanceof Error ? err.message : "Save failed",
},
});
} finally {
dispatch({ type: "saving", saving: false });
}
}, [brandId, state.tokenInput, state.defaultSheetIdInput, state.connectionEnabled]);
// ── Test connection ──
const handleTest = useCallback(async () => {
setTestResult({ state: "loading" });
const sheetIdInput =
state.defaultSheetIdInput.trim().length > 0
? state.defaultSheetIdInput.trim()
: state.workspace?.defaultSheetId ?? "";
if (!sheetIdInput) {
setTestResult({
state: "error",
message: "Enter a sheet ID or URL above to test against.",
});
return;
}
try {
const result = await testSmartsheetWorkspaceConnection(
brandId,
sheetIdInput,
state.tokenInput,
);
if (result.success) {
setTestResult({
state: "ok",
sheetName: result.sheetName,
columnCount: result.columnCount,
});
} else {
setTestResult({ state: "error", message: result.error });
}
} catch (err) {
setTestResult({
state: "error",
message: err instanceof Error ? err.message : "Test failed",
});
}
}, [brandId, state.tokenInput, state.defaultSheetIdInput, state.workspace?.defaultSheetId]);
// ── Disable (no-confirm shortcut) ──
const handleDisable = useCallback(async () => {
dispatch({ type: "saving", saving: true });
try {
await disableSmartsheetWorkspace(brandId);
const ws = await getSmartsheetWorkspace(brandId);
dispatch({ type: "load", workspace: ws });
dispatch({
type: "message",
message: { type: "success", text: "Connection disabled. Sheet mappings preserved." },
});
} catch (err) {
dispatch({
type: "message",
message: {
type: "error",
text: err instanceof Error ? err.message : "Disable failed",
},
});
} finally {
dispatch({ type: "saving", saving: false });
}
}, [brandId]);
if (state.loading) {
return (
<div className="rounded-xl border border-[#d4d9d3] bg-white p-6">
<p className="text-sm text-[#5a5d5a]">Loading workspace</p>
</div>
);
}
const isConnected = state.workspace?.configured === true;
const hasToken = state.workspace?.hasToken === true;
const maskedToken = state.workspace?.maskedToken ?? null;
return (
<div className="rounded-xl border border-[#d4d9d3] bg-white p-6">
<div className="flex items-start justify-between gap-4">
<div>
<h3
className="text-lg font-medium text-[#1a4d2e]"
style={{ fontFamily: "var(--font-display, Georgia, serif)" }}
>
Workbook connection
</h3>
<p className="mt-1 text-sm text-[#5a5d5a]">
One Smartsheet API token powers every feature below (water log, time
tracking, future payroll). Per-feature cards just pick which sheet
inside the workbook to write to.
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ${
isConnected && state.connectionEnabled
? "bg-[#dfe9d4] text-[#1a4d2e]"
: isConnected
? "bg-[#f1e8d4] text-[#8a6b3b]"
: "bg-[#f0e1d8] text-[#a4452b]"
}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
isConnected && state.connectionEnabled
? "bg-[#1a4d2e]"
: isConnected
? "bg-[#8a6b3b]"
: "bg-[#a4452b]"
}`}
/>
{isConnected
? state.connectionEnabled
? "Connected"
: "Paused"
: "Not connected"}
</span>
</div>
</div>
{/* ── Last verified ── */}
{isConnected && state.workspace?.lastTestAt && (
<p className="mt-3 text-xs text-[#5a5d5a]">
Last verified: {formatDateTime(new Date(state.workspace.lastTestAt))}
{state.workspace.lastTestError && (
<span className="ml-2 text-[#a4452b]">
· {state.workspace.lastTestError.slice(0, 80)}
</span>
)}
</p>
)}
{/* ── Form ── */}
<div className="mt-5 space-y-4">
<div>
<label className="block text-sm font-medium text-[#1a4d2e]">
API token
</label>
<div className="mt-1 flex items-center gap-2">
<input
type={state.showToken ? "text" : "password"}
autoComplete="off"
spellCheck={false}
className="flex-1 rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 text-sm text-[#1a4d2e] placeholder:text-[#8a8d8a] focus:border-[#1a4d2e] focus:outline-none focus:ring-1 focus:ring-[#1a4d2e]"
placeholder={
hasToken && maskedToken
? `Saved: ${maskedToken} — paste to rotate`
: "Paste your Smartsheet API token"
}
value={state.tokenInput}
onChange={(e) =>
dispatch({ type: "token", value: e.target.value })
}
data-test="smartsheet-workspace-token"
/>
<button
type="button"
className="rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 text-sm text-[#5a5d5a] hover:bg-[#fdfaf2]"
onClick={() =>
dispatch({ type: "showToken", value: !state.showToken })
}
>
{state.showToken ? "Hide" : "Show"}
</button>
</div>
<p className="mt-1 text-xs text-[#5a5d5a]">
Encrypted at rest with AES-256-GCM. Never returned to the browser
after save.
</p>
</div>
<div>
<label className="block text-sm font-medium text-[#1a4d2e]">
Default sheet ID (optional)
</label>
<input
type="text"
className="mt-1 w-full rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 text-sm text-[#1a4d2e] placeholder:text-[#8a8d8a] focus:border-[#1a4d2e] focus:outline-none focus:ring-1 focus:ring-[#1a4d2e]"
placeholder="e.g. 8309876543210123 (or paste a Smartsheet URL)"
value={state.defaultSheetIdInput}
onChange={(e) =>
dispatch({ type: "defaultSheet", value: e.target.value })
}
data-test="smartsheet-workspace-default-sheet"
/>
<p className="mt-1 text-xs text-[#5a5d5a]">
The &ldquo;home&rdquo; tab when the customer opens the workbook.
Per-feature cards below pick their own sheets independently.
</p>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-sm font-medium text-[#1a4d2e]">
<input
type="checkbox"
className="h-4 w-4 rounded border-[#d4d9d3] text-[#1a4d2e] focus:ring-[#1a4d2e]"
checked={state.connectionEnabled}
onChange={(e) =>
dispatch({ type: "connection", value: e.target.checked })
}
data-test="smartsheet-workspace-enabled"
/>
Connection enabled
</label>
<span className="text-xs text-[#5a5d5a]">
(Master switch pauses all per-feature syncs without losing mappings)
</span>
</div>
</div>
{/* ── Actions ── */}
<div className="mt-5 flex flex-wrap items-center gap-2">
<button
type="button"
className="rounded-lg bg-[#1a4d2e] px-4 py-2 text-sm font-medium text-white hover:bg-[#14432a] disabled:opacity-60"
onClick={handleSave}
disabled={state.saving}
data-test="smartsheet-workspace-save"
>
{state.saving ? "Saving…" : "Save"}
</button>
<button
type="button"
className="rounded-lg border border-[#1a4d2e] bg-white px-4 py-2 text-sm font-medium text-[#1a4d2e] hover:bg-[#fdfaf2] disabled:opacity-60"
onClick={handleTest}
disabled={testResult.state === "loading"}
data-test="smartsheet-workspace-test"
>
{testResult.state === "loading" ? "Testing…" : "Test connection"}
</button>
{isConnected && state.connectionEnabled && (
<button
type="button"
className="rounded-lg border border-[#d4d9d3] bg-white px-4 py-2 text-sm font-medium text-[#8a6b3b] hover:bg-[#fdfaf2] disabled:opacity-60"
onClick={handleDisable}
disabled={state.saving}
>
Pause connection
</button>
)}
</div>
{/* ── Test result ── */}
{testResult.state === "ok" && (
<div className="smartsheet-slide-down mt-4 rounded-lg border border-[#dfe9d4] bg-[#dfe9d4] p-3">
<p className="text-sm font-medium text-[#1a4d2e]">
Connected to &ldquo;{testResult.sheetName}&rdquo;
</p>
<p className="mt-1 text-xs text-[#5a5d5a]">
{testResult.columnCount} columns visible. The test uses the
{state.tokenInput.length > 0 ? " pasted" : " saved"} token.
</p>
</div>
)}
{testResult.state === "error" && (
<div className="smartsheet-slide-down mt-4 rounded-lg border border-[#f0e1d8] bg-[#f0e1d8] p-3">
<p className="text-sm font-medium text-[#a4452b]">
{testResult.message}
</p>
</div>
)}
{/* ── Save message ── */}
{state.message && (
<div
className={`smartsheet-fade-in mt-4 rounded-lg p-3 text-sm ${
state.message.type === "success"
? "bg-[#dfe9d4] text-[#1a4d2e]"
: "bg-[#f0e1d8] text-[#a4452b]"
}`}
>
{state.message.text}
</div>
)}
</div>
);
}
export default SmartsheetHub;
File diff suppressed because it is too large Load Diff