task-d.7.12
This commit is contained in:
181
alla-theme.css
Normal file
181
alla-theme.css
Normal file
@@ -0,0 +1,181 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9911 0 0);
|
||||
--foreground: oklch(0.2046 0 0);
|
||||
--card: oklch(0.9911 0 0);
|
||||
--card-foreground: oklch(0.2046 0 0);
|
||||
--popover: oklch(0.9911 0 0);
|
||||
--popover-foreground: oklch(0.4386 0 0);
|
||||
--primary: oklch(0.4799 0.0804 155.8197);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9940 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9461 0 0);
|
||||
--muted-foreground: oklch(0.2435 0 0);
|
||||
--accent: oklch(0.9461 0 0);
|
||||
--accent-foreground: oklch(0.2435 0 0);
|
||||
--destructive: oklch(0.5523 0.1927 32.7272);
|
||||
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||
--border: oklch(0.9037 0 0);
|
||||
--input: oklch(0.9731 0 0);
|
||||
--ring: oklch(0.5568 0.1355 155.8120);
|
||||
--chart-1: oklch(0.8348 0.1302 160.9080);
|
||||
--chart-2: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||
--sidebar: oklch(0.9911 0 0);
|
||||
--sidebar-foreground: oklch(0.5452 0 0);
|
||||
--sidebar-primary: oklch(0.5568 0.1355 155.8120);
|
||||
--sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||
--sidebar-accent: oklch(0.9461 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||
--sidebar-border: oklch(0.9037 0 0);
|
||||
--sidebar-ring: oklch(0.5568 0.1355 155.8120);
|
||||
--font-sans: Anek Kannada, ui-sans-serif, sans-serif, system-ui;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 5.5px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0.025em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.1822 0 0);
|
||||
--foreground: oklch(0.9288 0.0126 255.5078);
|
||||
--card: oklch(0.2046 0 0);
|
||||
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||
--popover: oklch(0.2603 0 0);
|
||||
--popover-foreground: oklch(0.7348 0 0);
|
||||
--primary: oklch(0.5568 0.1355 155.8120);
|
||||
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--secondary: oklch(0.2603 0 0);
|
||||
--secondary-foreground: oklch(0.9851 0 0);
|
||||
--muted: oklch(0.2393 0 0);
|
||||
--muted-foreground: oklch(0.7122 0 0);
|
||||
--accent: oklch(0.3132 0 0);
|
||||
--accent-foreground: oklch(0.9851 0 0);
|
||||
--destructive: oklch(0.3123 0.0852 29.7877);
|
||||
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||
--border: oklch(0.2809 0 0);
|
||||
--input: oklch(0.2603 0 0);
|
||||
--ring: oklch(0.5568 0.1355 155.8120);
|
||||
--chart-1: oklch(0.8003 0.1821 151.7110);
|
||||
--chart-2: oklch(0.7137 0.1434 254.6240);
|
||||
--chart-3: oklch(0.7090 0.1592 293.5412);
|
||||
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||
--chart-5: oklch(0.7845 0.1325 181.9120);
|
||||
--sidebar: oklch(0.1822 0 0);
|
||||
--sidebar-foreground: oklch(0.6301 0 0);
|
||||
--sidebar-primary: oklch(0.5568 0.1355 155.8120);
|
||||
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--sidebar-accent: oklch(0.3132 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-border: oklch(0.2809 0 0);
|
||||
--sidebar-ring: oklch(0.5568 0.1355 155.8120);
|
||||
--font-sans: Anek Kannada, ui-sans-serif, sans-serif, system-ui;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 5.5px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0px 1px 5.5px 0px hsl(0 0% 0% / 0.25);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Task D.7.12 Step-by-Step First-Run Required CSV Import
|
||||
|
||||
Date: 2026-07-03
|
||||
Status: implemented
|
||||
|
||||
## Review Completed
|
||||
|
||||
- Reviewed `AGENTS.md` task governance from the active repository instructions.
|
||||
- Reviewed `docs/standards/task-contract-template.md`.
|
||||
- Reviewed `docs/standards/task-catalog.md`.
|
||||
- Reviewed `docs/standards/project-foundations.md`.
|
||||
- Reviewed `docs/standards/architecture-rules.md`.
|
||||
- Reviewed `docs/standards/ui-ux-rules.md`.
|
||||
- Reviewed `docs/standards/task-review-checklist.md`.
|
||||
- Reviewed setup lineage notes:
|
||||
- `docs/implementation/task-d7-initial-system-setup-audit.md`
|
||||
- `docs/implementation/task-d76-dashboard-setup-wizard-ui.md`
|
||||
- `docs/implementation/task-d78-seed-manifest-setup-state-persistence.md`
|
||||
- `docs/setup/setup-wizard-blueprint.md`
|
||||
|
||||
## Decision
|
||||
|
||||
The required CSV order is:
|
||||
|
||||
1. `users.csv`
|
||||
2. `organizations.csv`
|
||||
3. `memberships.csv`
|
||||
4. `crm-role-assignments.csv`
|
||||
5. `master-options.csv`
|
||||
6. `branches.csv`
|
||||
7. `product-types.csv`
|
||||
8. `document-sequences.csv`
|
||||
9. `approval-workflows.csv`
|
||||
10. `approval-steps.csv`
|
||||
11. `approval-matrix.csv`
|
||||
|
||||
This intentionally uses `users.csv` before `organizations.csv` because `organizations.csv`
|
||||
resolves `created_by_email` against existing users. This follows the recommendation in
|
||||
`plans/task-d.7.12.md` and avoids introducing a temporary bootstrap actor into the
|
||||
organization import data path.
|
||||
|
||||
## Scope Delivered
|
||||
|
||||
- Added server-owned bootstrap required step metadata in `setup-bootstrap.service.ts`.
|
||||
- Added `crm-role-assignments.csv` to the required bootstrap file list.
|
||||
- Changed public bootstrap preview and commit APIs to accept one expected CSV file per step.
|
||||
- Persisted each required CSV result to its own `setup_run_steps.step_key`.
|
||||
- Updated bootstrap completion to require all required step keys to be `passed` or `warning`.
|
||||
- Added bootstrap step dependency staleness rules in setup run persistence.
|
||||
- Reworked the Administration Setup Wizard UI into a left-stepper, single-step import flow.
|
||||
- Kept optional business CSV imports separate from system-required steps.
|
||||
|
||||
## Reuse
|
||||
|
||||
- Reused existing CSV preview and commit engine.
|
||||
- Reused existing `/api/setup/bootstrap/preview`, `/api/setup/bootstrap/commit`, and
|
||||
`/api/setup/bootstrap/complete` route groups.
|
||||
- Reused existing `setup_runs` and `setup_run_steps` persistence.
|
||||
- Reused setup template metadata from `setup/templates/templates.json`.
|
||||
- Reused shadcn/ui dashboard primitives and existing icon registry.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm run typecheck -- --pretty false` passed.
|
||||
|
||||
173
onvalla-theme.css
Normal file
173
onvalla-theme.css
Normal file
@@ -0,0 +1,173 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9816 0.0017 247.8390);
|
||||
--foreground: oklch(0.2277 0.0534 259.0168);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.2277 0.0534 259.0168);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.2277 0.0534 259.0168);
|
||||
--primary: oklch(0.3863 0.0858 321.9713);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.6340 0.2116 31.7828);
|
||||
--secondary-foreground: oklch(1.0000 0 0);
|
||||
--muted: oklch(0.9275 0.0017 247.8404);
|
||||
--muted-foreground: oklch(0.5751 0.0259 285.5573);
|
||||
--accent: oklch(0.6340 0.2116 31.7828);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0.6340 0.2116 31.7828);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0.5751 0.0259 285.5573);
|
||||
--input: oklch(0.9275 0.0017 247.8404);
|
||||
--ring: oklch(0.3863 0.0858 321.9713);
|
||||
--chart-1: oklch(0.3863 0.0858 321.9713);
|
||||
--chart-2: oklch(0.6340 0.2116 31.7828);
|
||||
--chart-3: oklch(0.2277 0.0534 259.0168);
|
||||
--chart-4: oklch(0.5751 0.0259 285.5573);
|
||||
--chart-5: oklch(0.2185 0.0702 317.5316);
|
||||
--sidebar: oklch(1.0000 0 0);
|
||||
--sidebar-foreground: oklch(0.2277 0.0534 259.0168);
|
||||
--sidebar-primary: oklch(0.3863 0.0858 321.9713);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.9275 0.0017 247.8404);
|
||||
--sidebar-accent-foreground: oklch(0.2277 0.0534 259.0168);
|
||||
--sidebar-border: oklch(0.9275 0.0017 247.8404);
|
||||
--sidebar-ring: oklch(0.3863 0.0858 321.9713);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 2px;
|
||||
--shadow-blur: 10px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.1;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 2px 10px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 2px 10px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0px 2px 10px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow: 0px 2px 10px 0px hsl(0 0% 0% / 0.10), 0px 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-md: 0px 2px 10px 0px hsl(0 0% 0% / 0.10), 0px 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-lg: 0px 2px 10px 0px hsl(0 0% 0% / 0.10), 0px 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0px 2px 10px 0px hsl(0 0% 0% / 0.10), 0px 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0px 2px 10px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.2277 0.0534 259.0168);
|
||||
--foreground: oklch(0.9275 0.0017 247.8404);
|
||||
--card: oklch(0.2185 0.0702 317.5316);
|
||||
--card-foreground: oklch(1.0000 0 0);
|
||||
--popover: oklch(0.2185 0.0702 317.5316);
|
||||
--popover-foreground: oklch(1.0000 0 0);
|
||||
--primary: oklch(0.3863 0.0858 321.9713);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.6340 0.2116 31.7828);
|
||||
--secondary-foreground: oklch(1.0000 0 0);
|
||||
--muted: oklch(0.5751 0.0259 285.5573);
|
||||
--muted-foreground: oklch(0.9275 0.0017 247.8404);
|
||||
--accent: oklch(0.6340 0.2116 31.7828);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0.6340 0.2116 31.7828);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0.3863 0.0858 321.9713);
|
||||
--input: oklch(0.2277 0.0534 259.0168);
|
||||
--ring: oklch(0.6340 0.2116 31.7828);
|
||||
--chart-1: oklch(0.3863 0.0858 321.9713);
|
||||
--chart-2: oklch(0.6340 0.2116 31.7828);
|
||||
--chart-3: oklch(0.5751 0.0259 285.5573);
|
||||
--chart-4: oklch(0.2185 0.0702 317.5316);
|
||||
--chart-5: oklch(1.0000 0 0);
|
||||
--sidebar: oklch(0.2185 0.0702 317.5316);
|
||||
--sidebar-foreground: oklch(0.9275 0.0017 247.8404);
|
||||
--sidebar-primary: oklch(0.6340 0.2116 31.7828);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.3863 0.0858 321.9713);
|
||||
--sidebar-accent-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-border: oklch(0.3863 0.0858 321.9713);
|
||||
--sidebar-ring: oklch(0.6340 0.2116 31.7828);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 4px;
|
||||
--shadow-blur: 12px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.3;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 4px 12px 0px hsl(0 0% 0% / 0.15);
|
||||
--shadow-xs: 0px 4px 12px 0px hsl(0 0% 0% / 0.15);
|
||||
--shadow-sm: 0px 4px 12px 0px hsl(0 0% 0% / 0.30), 0px 1px 2px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow: 0px 4px 12px 0px hsl(0 0% 0% / 0.30), 0px 1px 2px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-md: 0px 4px 12px 0px hsl(0 0% 0% / 0.30), 0px 2px 4px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-lg: 0px 4px 12px 0px hsl(0 0% 0% / 0.30), 0px 4px 6px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-xl: 0px 4px 12px 0px hsl(0 0% 0% / 0.30), 0px 8px 10px -1px hsl(0 0% 0% / 0.30);
|
||||
--shadow-2xl: 0px 4px 12px 0px hsl(0 0% 0% / 0.75);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
355
plans/task-d.7.12.md
Normal file
355
plans/task-d.7.12.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Task D.7.12 – Step-by-Step First-Run Required CSV Import
|
||||
|
||||
## Objective
|
||||
|
||||
Improve First-run Bootstrap Setup so required system CSV files are imported step-by-step instead of uploading all files at once.
|
||||
|
||||
The goal is to guide the user through each required setup dependency in the correct order:
|
||||
|
||||
Step 1 organizations.csv
|
||||
Step 2 users.csv
|
||||
Step 3 memberships.csv
|
||||
Step 4 crm-role-assignments.csv
|
||||
Step 5 master-options.csv
|
||||
Step 6 branches.csv
|
||||
Step 7 product-types.csv
|
||||
Step 8 document-sequences.csv
|
||||
Step 9 approval-workflows.csv
|
||||
Step 10 approval-steps.csv
|
||||
Step 11 approval-matrix.csv
|
||||
|
||||
After all required steps pass, optional business CSV import can be shown separately.
|
||||
|
||||
---
|
||||
|
||||
## Desired UX
|
||||
|
||||
Route:
|
||||
|
||||
/administration/setup
|
||||
|
||||
Show First-run Bootstrap Wizard.
|
||||
|
||||
Each step has:
|
||||
|
||||
- CSV file name
|
||||
- purpose
|
||||
- required columns
|
||||
- upload area
|
||||
- preview button
|
||||
- run/import button
|
||||
- result summary
|
||||
- PASS / WARNING / FAIL status
|
||||
- next button
|
||||
|
||||
User flow:
|
||||
|
||||
1. Upload organizations.csv
|
||||
2. Preview
|
||||
3. Run Import
|
||||
4. Step marked PASS
|
||||
5. Move to users.csv
|
||||
6. Repeat until all system-required CSV files are complete
|
||||
7. Show final verification
|
||||
8. Mark setup completed
|
||||
9. Redirect to login
|
||||
|
||||
---
|
||||
|
||||
## Required Import Steps
|
||||
|
||||
Use exact dependency order:
|
||||
|
||||
1. organizations.csv
|
||||
2. users.csv
|
||||
3. memberships.csv
|
||||
4. crm-role-assignments.csv
|
||||
5. master-options.csv
|
||||
6. branches.csv
|
||||
7. product-types.csv
|
||||
8. document-sequences.csv
|
||||
9. approval-workflows.csv
|
||||
10. approval-steps.csv
|
||||
11. approval-matrix.csv
|
||||
|
||||
Important:
|
||||
|
||||
If current import engine requires users before organizations because organizations.created_by_email resolves to users, support one of these approaches:
|
||||
|
||||
Option A:
|
||||
Use temporary bootstrap actor for organizations.csv.
|
||||
|
||||
Option B:
|
||||
Change order to users.csv first.
|
||||
|
||||
Decision must be documented before implementation.
|
||||
|
||||
Recommended decision:
|
||||
|
||||
Step 1 users.csv
|
||||
Step 2 organizations.csv
|
||||
Step 3 memberships.csv
|
||||
|
||||
because organizations.csv currently requires created_by_email.
|
||||
|
||||
---
|
||||
|
||||
## System Required Group
|
||||
|
||||
Define a bootstrap required group:
|
||||
|
||||
system_required
|
||||
|
||||
Files:
|
||||
|
||||
- users.csv
|
||||
- organizations.csv
|
||||
- memberships.csv
|
||||
- crm-role-assignments.csv
|
||||
- master-options.csv
|
||||
- branches.csv
|
||||
- product-types.csv
|
||||
- document-sequences.csv
|
||||
- approval-workflows.csv
|
||||
- approval-steps.csv
|
||||
- approval-matrix.csv
|
||||
|
||||
Business optional group:
|
||||
|
||||
business_optional
|
||||
|
||||
Files:
|
||||
|
||||
- customers.csv
|
||||
- contacts.csv
|
||||
- contact-shares.csv
|
||||
- leads.csv
|
||||
- opportunities.csv
|
||||
- opportunity-parties.csv
|
||||
- quotations.csv
|
||||
- quotation-items.csv
|
||||
- quotation-parties.csv
|
||||
- quotation-topics.csv
|
||||
- quotation-topic-items.csv
|
||||
|
||||
---
|
||||
|
||||
## Implementation Scope
|
||||
|
||||
Update First-run Bootstrap UI:
|
||||
|
||||
src/app/administration/setup/page.tsx
|
||||
|
||||
or bootstrap setup component under:
|
||||
|
||||
src/features/setup/components/bootstrap-setup-wizard.tsx
|
||||
|
||||
Add step configuration:
|
||||
|
||||
src/features/setup/bootstrap/bootstrap-required-steps.ts
|
||||
|
||||
Each step defines:
|
||||
|
||||
- stepKey
|
||||
- fileName
|
||||
- title
|
||||
- description
|
||||
- required
|
||||
- dependsOn
|
||||
- template metadata
|
||||
- status
|
||||
- previewHash
|
||||
- import report
|
||||
|
||||
---
|
||||
|
||||
## API Reuse
|
||||
|
||||
Reuse existing APIs:
|
||||
|
||||
POST /api/setup/bootstrap/preview
|
||||
POST /api/setup/bootstrap/commit
|
||||
|
||||
Each step submits one CSV file at a time.
|
||||
|
||||
Do not create new import logic.
|
||||
|
||||
Commit flow per step:
|
||||
|
||||
1. Upload one file
|
||||
2. Preview
|
||||
3. Store previewHash for that step
|
||||
4. Commit same file with previewHash
|
||||
5. Persist step result
|
||||
6. Move to next step
|
||||
|
||||
---
|
||||
|
||||
## Persistence
|
||||
|
||||
Persist step status in setup_run_steps.
|
||||
|
||||
Step keys:
|
||||
|
||||
bootstrap_users
|
||||
bootstrap_organizations
|
||||
bootstrap_memberships
|
||||
bootstrap_crm_role_assignments
|
||||
bootstrap_master_options
|
||||
bootstrap_branches
|
||||
bootstrap_product_types
|
||||
bootstrap_document_sequences
|
||||
bootstrap_approval_workflows
|
||||
bootstrap_approval_steps
|
||||
bootstrap_approval_matrix
|
||||
|
||||
Each step stores safe output only.
|
||||
|
||||
No raw CSV contents.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Rules
|
||||
|
||||
A step is locked until all dependsOn steps pass.
|
||||
|
||||
Example:
|
||||
|
||||
memberships.csv depends on:
|
||||
|
||||
- users.csv
|
||||
- organizations.csv
|
||||
|
||||
crm-role-assignments.csv depends on:
|
||||
|
||||
- users.csv
|
||||
- organizations.csv
|
||||
- memberships.csv
|
||||
- master-options.csv
|
||||
- branches.csv
|
||||
- product-types.csv
|
||||
|
||||
document-sequences.csv depends on:
|
||||
|
||||
- organizations.csv
|
||||
- branches.csv
|
||||
- product-types.csv
|
||||
|
||||
approval-steps.csv depends on:
|
||||
|
||||
- approval-workflows.csv
|
||||
|
||||
approval-matrix.csv depends on:
|
||||
|
||||
- approval-workflows.csv
|
||||
- branches.csv
|
||||
- product-types.csv
|
||||
|
||||
---
|
||||
|
||||
## UI Requirements
|
||||
|
||||
Show left stepper:
|
||||
|
||||
System Required
|
||||
|
||||
- Users
|
||||
- Organizations
|
||||
- Memberships
|
||||
- CRM Roles
|
||||
- Master Options
|
||||
- Branches
|
||||
- Product Types
|
||||
- Document Sequences
|
||||
- Approval Workflows
|
||||
- Approval Steps
|
||||
- Approval Matrix
|
||||
|
||||
Optional Business Data
|
||||
|
||||
- Customers
|
||||
- Contacts
|
||||
- Leads
|
||||
- Opportunities
|
||||
- Quotations
|
||||
|
||||
Each step panel shows:
|
||||
|
||||
- What this file does
|
||||
- Required filename
|
||||
- Required headers
|
||||
- Sample template reference
|
||||
- Upload control
|
||||
- Preview result
|
||||
- Import result
|
||||
- Errors and suggested fixes
|
||||
|
||||
---
|
||||
|
||||
## Completion Rule
|
||||
|
||||
Setup can complete only when all system_required steps are PASS or WARNING acknowledged.
|
||||
|
||||
Then run:
|
||||
|
||||
POST /api/setup/bootstrap/complete
|
||||
|
||||
Completion must verify:
|
||||
|
||||
- super_admin exists
|
||||
- organization exists
|
||||
- membership exists
|
||||
- CRM role assignment exists
|
||||
- branch exists
|
||||
- product type exists
|
||||
- document sequence exists
|
||||
- approval workflow/matrix exists
|
||||
|
||||
After success:
|
||||
|
||||
redirect to /auth/sign-in
|
||||
|
||||
---
|
||||
|
||||
## Optional Business Import
|
||||
|
||||
After system_required completion, show optional step:
|
||||
|
||||
Import Business Data
|
||||
|
||||
Allow either:
|
||||
|
||||
- step-by-step business import
|
||||
or
|
||||
- batch upload all business CSV
|
||||
|
||||
This can be deferred if needed.
|
||||
|
||||
---
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Changing CSV template format
|
||||
- Rewriting import engine
|
||||
- Dashboard setup wizard redesign
|
||||
- Reset/reseed
|
||||
- Plugin runtime changes
|
||||
- Business import advanced UI
|
||||
- Production public setup after completion
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- First-run setup imports required CSV files step-by-step.
|
||||
- Each step accepts only the expected CSV filename.
|
||||
- Each step has Preview and Run buttons.
|
||||
- Import cannot run without successful previewHash.
|
||||
- Next step is locked until dependencies pass.
|
||||
- Step status persists across refresh.
|
||||
- System cannot complete until all required steps pass.
|
||||
- Existing batch import engine remains unchanged.
|
||||
- No raw CSV contents are persisted.
|
||||
- Setup completion redirects to login.
|
||||
- After completion, public bootstrap setup is blocked.
|
||||
- Typecheck passes.
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { commitSetupCsvImport } from '@/features/setup/server/csv-commit.service';
|
||||
import { createCsvImportSeedManifest } from '@/features/setup/server/seed-manifest.service';
|
||||
import {
|
||||
assertBootstrapRequiredFiles,
|
||||
assertBootstrapStepFiles,
|
||||
assertBootstrapSetupAvailable,
|
||||
assertBootstrapToken,
|
||||
BOOTSTRAP_ACTOR_ID,
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
import { saveSetupStep, startSetupRun } from '@/features/setup/server/setup-state.service';
|
||||
|
||||
function isFile(value: FormDataEntryValue): value is File {
|
||||
return typeof value === 'object' && 'arrayBuffer' in value && 'name' in value;
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
'arrayBuffer' in value &&
|
||||
'name' in value &&
|
||||
typeof value.name === 'string' &&
|
||||
value.name.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function parseBoolean(value: FormDataEntryValue | null): boolean {
|
||||
@@ -28,6 +34,7 @@ export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
assertBootstrapToken(String(formData.get('bootstrapToken') ?? request.headers.get('x-bootstrap-setup-token') ?? ''));
|
||||
|
||||
const stepKey = String(formData.get('stepKey') ?? '');
|
||||
const previewHash = String(formData.get('previewHash') ?? '');
|
||||
const dryRun = parseBoolean(formData.get('dryRun'));
|
||||
const uploadedFiles = Array.from(formData.values()).filter(isFile);
|
||||
@@ -43,7 +50,7 @@ export async function POST(request: NextRequest) {
|
||||
}))
|
||||
);
|
||||
|
||||
assertBootstrapRequiredFiles(files);
|
||||
const step = assertBootstrapStepFiles(stepKey, files);
|
||||
await startSetupRun({ actorId: BOOTSTRAP_ACTOR_ID, mode: 'production', targetOrganizationId: null });
|
||||
|
||||
const report = await commitSetupCsvImport({
|
||||
@@ -58,7 +65,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
await saveSetupStep({
|
||||
stepKey: 'csv_commit',
|
||||
stepKey: step.stepKey,
|
||||
status: setupStepStatusFromImportReport(report),
|
||||
inputHash: report.committedHash,
|
||||
output: toSerializableRecord(report),
|
||||
@@ -72,6 +79,10 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ message: error.message, details: error.details }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof TypeError && /multipart\/form-data|application\/x-www-form-urlencoded/i.test(error.message)) {
|
||||
return NextResponse.json({ message: 'Upload setup CSV files using multipart/form-data.' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable commit bootstrap setup CSV import' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
assertBootstrapSetupAvailable,
|
||||
assertBootstrapToken,
|
||||
BOOTSTRAP_ACTOR_ID,
|
||||
BOOTSTRAP_REQUIRED_STEPS,
|
||||
BootstrapSetupError,
|
||||
getBootstrapSetupStatus
|
||||
} from '@/features/setup/server/setup-bootstrap.service';
|
||||
@@ -16,15 +17,23 @@ export async function POST(request: NextRequest) {
|
||||
assertBootstrapToken(body.bootstrapToken ?? request.headers.get('x-bootstrap-setup-token') ?? '');
|
||||
|
||||
const currentRun = await getCurrentSetupRun();
|
||||
const commitStep = currentRun.steps.find((step) => step.stepKey === 'csv_commit');
|
||||
const hasSuccessfulCommit =
|
||||
currentRun.run?.status === 'csv_imported' ||
|
||||
currentRun.run?.status === 'verified' ||
|
||||
commitStep?.status === 'passed' ||
|
||||
commitStep?.status === 'warning';
|
||||
const completedStepKeys = new Set(
|
||||
currentRun.steps
|
||||
.filter((step) => step.status === 'passed' || step.status === 'warning')
|
||||
.map((step) => step.stepKey)
|
||||
);
|
||||
const missingSteps = BOOTSTRAP_REQUIRED_STEPS.filter(
|
||||
(step) => !completedStepKeys.has(step.stepKey)
|
||||
).map((step) => step.stepKey);
|
||||
|
||||
if (!hasSuccessfulCommit) {
|
||||
return NextResponse.json({ message: 'Commit bootstrap setup data before completing setup.' }, { status: 409 });
|
||||
if (missingSteps.length > 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Complete all required bootstrap CSV steps before completing setup.',
|
||||
missingSteps
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
const status = await getBootstrapSetupStatus();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { previewSetupCsvImport } from '@/features/setup/server/csv-preview.service';
|
||||
import {
|
||||
assertBootstrapRequiredFiles,
|
||||
assertBootstrapStepFiles,
|
||||
assertBootstrapSetupAvailable,
|
||||
assertBootstrapToken,
|
||||
BOOTSTRAP_ACTOR_ID,
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
import { saveSetupStep, startSetupRun } from '@/features/setup/server/setup-state.service';
|
||||
|
||||
function isFile(value: FormDataEntryValue): value is File {
|
||||
return typeof value === 'object' && 'arrayBuffer' in value && 'name' in value;
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
'arrayBuffer' in value &&
|
||||
'name' in value &&
|
||||
typeof value.name === 'string' &&
|
||||
value.name.trim().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -22,6 +28,7 @@ export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
assertBootstrapToken(String(formData.get('bootstrapToken') ?? request.headers.get('x-bootstrap-setup-token') ?? ''));
|
||||
|
||||
const stepKey = String(formData.get('stepKey') ?? '');
|
||||
const uploadedFiles = Array.from(formData.values()).filter(isFile);
|
||||
|
||||
if (uploadedFiles.length === 0) {
|
||||
@@ -35,13 +42,13 @@ export async function POST(request: NextRequest) {
|
||||
}))
|
||||
);
|
||||
|
||||
assertBootstrapRequiredFiles(files);
|
||||
const step = assertBootstrapStepFiles(stepKey, files);
|
||||
|
||||
await startSetupRun({ actorId: BOOTSTRAP_ACTOR_ID, mode: 'production', targetOrganizationId: null });
|
||||
const preview = await previewSetupCsvImport(files);
|
||||
|
||||
await saveSetupStep({
|
||||
stepKey: 'csv_preview',
|
||||
stepKey: step.stepKey,
|
||||
status: setupStepStatusFromPreview(preview),
|
||||
inputHash: preview.previewHash,
|
||||
output: toSerializableRecord(preview),
|
||||
@@ -49,12 +56,24 @@ export async function POST(request: NextRequest) {
|
||||
warnings: preview.files.flatMap((file) => file.warnings)
|
||||
});
|
||||
|
||||
return NextResponse.json(preview, { status: preview.summary.error > 0 ? 400 : 200 });
|
||||
return NextResponse.json(preview);
|
||||
} catch (error) {
|
||||
if (error instanceof BootstrapSetupError) {
|
||||
return NextResponse.json({ message: error.message, details: error.details }, { status: error.status });
|
||||
}
|
||||
|
||||
if (error instanceof TypeError && /multipart\/form-data|application\/x-www-form-urlencoded/i.test(error.message)) {
|
||||
return NextResponse.json({ message: 'Upload setup CSV files using multipart/form-data.' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.error('Bootstrap preview failed');
|
||||
if (error instanceof Error) {
|
||||
for (const line of (error.stack ?? error.message).split('\n').slice(0, 12)) {
|
||||
console.error(line);
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable preview bootstrap setup CSV import' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,20 @@
|
||||
/**
|
||||
* Default theme that loads when no user preference is set
|
||||
* Change this value to set a different default theme
|
||||
* Default theme loads when no user preference is set.
|
||||
* Change value to set a different default theme.
|
||||
*/
|
||||
export const DEFAULT_THEME = 'mongodb';
|
||||
export const DEFAULT_THEME = 'alla';
|
||||
|
||||
export const THEMES = [
|
||||
{
|
||||
name: 'MongoDB',
|
||||
value: 'mongodb'
|
||||
},
|
||||
{
|
||||
name: 'Claude',
|
||||
value: 'claude'
|
||||
},
|
||||
{
|
||||
name: 'Neobrutualism',
|
||||
value: 'neobrutualism'
|
||||
},
|
||||
{
|
||||
name: 'Supabase',
|
||||
value: 'supabase'
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
value: 'vercel'
|
||||
},
|
||||
{
|
||||
name: 'Mono',
|
||||
value: 'mono'
|
||||
},
|
||||
{
|
||||
name: 'Notebook',
|
||||
value: 'notebook'
|
||||
},
|
||||
{
|
||||
name: 'Light Green',
|
||||
value: 'light-green'
|
||||
},
|
||||
{
|
||||
name: 'Zen',
|
||||
value: 'zen'
|
||||
},
|
||||
{
|
||||
name: 'Astro Vista',
|
||||
value: 'astro-vista'
|
||||
},
|
||||
{
|
||||
name: 'WhatsApp',
|
||||
value: 'whatsapp'
|
||||
}
|
||||
{ name: 'MongoDB', value: 'mongodb' },
|
||||
{ name: 'Claude', value: 'claude' },
|
||||
{ name: 'Neobrutualism', value: 'neobrutualism' },
|
||||
{ name: 'Supabase', value: 'supabase' },
|
||||
{ name: 'Vercel', value: 'vercel' },
|
||||
{ name: 'Mono', value: 'mono' },
|
||||
{ name: 'Notebook', value: 'notebook' },
|
||||
{ name: 'Light Green', value: 'light-green' },
|
||||
{ name: 'Zen', value: 'zen' },
|
||||
{ name: 'Astro Vista', value: 'astro-vista' },
|
||||
{ name: 'WhatsApp', value: 'whatsapp' },
|
||||
{ name: 'ALLA', value: 'alla' }
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import {
|
||||
getBootstrapSetupStatus,
|
||||
getCurrentSetupRun,
|
||||
getSetupProfiles,
|
||||
getSetupReadiness,
|
||||
@@ -14,6 +15,7 @@ export const setupKeys = {
|
||||
readiness: (profileId?: string) => [...setupKeys.all, 'readiness', profileId ?? 'crm_demo'] as const,
|
||||
templates: (profileId?: string) => [...setupKeys.all, 'templates', profileId ?? 'crm_demo'] as const,
|
||||
profiles: (profileId?: string) => [...setupKeys.all, 'profiles', profileId ?? 'crm_demo'] as const,
|
||||
bootstrapStatus: () => [...setupKeys.all, 'bootstrap', 'status'] as const,
|
||||
run: () => [...setupKeys.all, 'run'] as const,
|
||||
report: () => [...setupKeys.all, 'run', 'report'] as const,
|
||||
verification: (payload: SetupVerificationPayload = {}) =>
|
||||
@@ -38,6 +40,12 @@ export const setupProfilesQueryOptions = (profileId?: string) =>
|
||||
queryFn: () => getSetupProfiles(profileId)
|
||||
});
|
||||
|
||||
export const bootstrapSetupStatusQueryOptions = () =>
|
||||
queryOptions({
|
||||
queryKey: setupKeys.bootstrapStatus(),
|
||||
queryFn: () => getBootstrapSetupStatus()
|
||||
});
|
||||
|
||||
export const setupRunQueryOptions = () =>
|
||||
queryOptions({
|
||||
queryKey: setupKeys.run(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiClientError, apiClient } from '@/lib/api-client';
|
||||
import type {
|
||||
CsvPreviewResult,
|
||||
BootstrapSetupStatusResponse,
|
||||
ImportReport,
|
||||
SaveSetupStepPayload,
|
||||
SetupProfilesResponse,
|
||||
@@ -34,6 +35,10 @@ export async function getSetupProfiles(profileId?: string): Promise<SetupProfile
|
||||
return apiClient<SetupProfilesResponse>(withProfile('/setup/profiles', profileId));
|
||||
}
|
||||
|
||||
export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatusResponse> {
|
||||
return apiClient<BootstrapSetupStatusResponse>('/setup/bootstrap/status');
|
||||
}
|
||||
|
||||
export async function getCurrentSetupRun(): Promise<SetupRunResponse> {
|
||||
return apiClient<SetupRunResponse>('/setup/run/current');
|
||||
}
|
||||
@@ -102,6 +107,23 @@ export async function previewSetupCsvFiles(files: File[]): Promise<CsvPreviewRes
|
||||
);
|
||||
}
|
||||
|
||||
export async function previewBootstrapCsvStep(input: {
|
||||
stepKey: string;
|
||||
file: File;
|
||||
bootstrapToken?: string;
|
||||
}): Promise<CsvPreviewResult> {
|
||||
const formData = buildCsvFormData([input.file]);
|
||||
formData.set('stepKey', input.stepKey);
|
||||
if (input.bootstrapToken) formData.set('bootstrapToken', input.bootstrapToken);
|
||||
|
||||
return readUploadResponse<CsvPreviewResult>(
|
||||
await fetch('/api/setup/bootstrap/preview', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function commitSetupCsvFiles(input: {
|
||||
files: File[];
|
||||
previewHash: string;
|
||||
@@ -118,3 +140,31 @@ export async function commitSetupCsvFiles(input: {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function commitBootstrapCsvStep(input: {
|
||||
stepKey: string;
|
||||
file: File;
|
||||
previewHash: string;
|
||||
dryRun?: boolean;
|
||||
bootstrapToken?: string;
|
||||
}): Promise<ImportReport> {
|
||||
const formData = buildCsvFormData([input.file]);
|
||||
formData.set('stepKey', input.stepKey);
|
||||
formData.set('previewHash', input.previewHash);
|
||||
formData.set('dryRun', String(Boolean(input.dryRun)));
|
||||
if (input.bootstrapToken) formData.set('bootstrapToken', input.bootstrapToken);
|
||||
|
||||
return readUploadResponse<ImportReport>(
|
||||
await fetch('/api/setup/bootstrap/commit', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function completeBootstrapSetup(report: Record<string, unknown>): Promise<SetupRunResponse> {
|
||||
return apiClient<SetupRunResponse>('/setup/bootstrap/complete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ report })
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +99,28 @@ export interface SetupTemplatesResponse {
|
||||
templates: SetupTemplateMetadataItem[];
|
||||
}
|
||||
|
||||
export interface BootstrapRequiredStepDto {
|
||||
stepKey: string;
|
||||
fileName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
dependsOn: string[];
|
||||
}
|
||||
|
||||
export interface BootstrapSetupStatusResponse {
|
||||
initialized: boolean;
|
||||
tokenRequired: boolean;
|
||||
checks: {
|
||||
completedSetupRun: boolean;
|
||||
superAdminUser: boolean;
|
||||
organization: boolean;
|
||||
adminMembership: boolean;
|
||||
};
|
||||
requiredFiles: string[];
|
||||
optionalFiles: string[];
|
||||
requiredSteps: BootstrapRequiredStepDto[];
|
||||
}
|
||||
|
||||
export interface SetupProfilesResponse {
|
||||
success: boolean;
|
||||
time: string;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { commitSetupCsvFiles, previewSetupCsvFiles } from './service';
|
||||
import {
|
||||
commitBootstrapCsvStep,
|
||||
commitSetupCsvFiles,
|
||||
previewBootstrapCsvStep,
|
||||
previewSetupCsvFiles
|
||||
} from './service';
|
||||
|
||||
export function useCsvPreview() {
|
||||
return useMutation({
|
||||
@@ -15,3 +20,22 @@ export function useCsvCommit() {
|
||||
commitSetupCsvFiles(input)
|
||||
});
|
||||
}
|
||||
|
||||
export function useBootstrapCsvPreview() {
|
||||
return useMutation({
|
||||
mutationFn: (input: { stepKey: string; file: File; bootstrapToken?: string }) =>
|
||||
previewBootstrapCsvStep(input)
|
||||
});
|
||||
}
|
||||
|
||||
export function useBootstrapCsvCommit() {
|
||||
return useMutation({
|
||||
mutationFn: (input: {
|
||||
stepKey: string;
|
||||
file: File;
|
||||
previewHash: string;
|
||||
dryRun?: boolean;
|
||||
bootstrapToken?: string;
|
||||
}) => commitBootstrapCsvStep(input)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { completeSetupRun, saveSetupStep, startSetupRun } from './service';
|
||||
import { completeBootstrapSetup, completeSetupRun, saveSetupStep, startSetupRun } from './service';
|
||||
import { setupKeys } from './queries';
|
||||
import type { SaveSetupStepPayload } from './types';
|
||||
|
||||
@@ -40,3 +40,12 @@ export function useCompleteSetupRun() {
|
||||
onSuccess: invalidate
|
||||
});
|
||||
}
|
||||
|
||||
export function useCompleteBootstrapSetup() {
|
||||
const invalidate = useInvalidateSetupRun();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (report: Record<string, unknown>) => completeBootstrapSetup(report),
|
||||
onSuccess: invalidate
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -883,11 +883,11 @@ export function buildCsvPreviewBatchIndex(files: CsvParsedFile[]): CsvPreviewBat
|
||||
files.forEach((file) => {
|
||||
file.rows.forEach((row) => {
|
||||
const values = row.values;
|
||||
const organizationCode = normalizeOrganizationCode(values.organization_code);
|
||||
const organizationCode = values.organization_code ? normalizeOrganizationCode(values.organization_code) : '';
|
||||
|
||||
switch (file.template.file) {
|
||||
case 'organizations.csv':
|
||||
index.organizations.add(normalizeOrganizationCode(values.organization_code));
|
||||
if (values.organization_code) index.organizations.add(normalizeOrganizationCode(values.organization_code));
|
||||
break;
|
||||
case 'users.csv':
|
||||
index.users.add(normalizeEmail(values.email));
|
||||
|
||||
@@ -27,7 +27,7 @@ function issue(
|
||||
}
|
||||
|
||||
function organizationCode(values: Record<string, string>): string {
|
||||
return normalizeOrganizationCode(values.organization_code);
|
||||
return values.organization_code ? normalizeOrganizationCode(values.organization_code) : '';
|
||||
}
|
||||
|
||||
function businessKey(file: CsvParsedFile, values: Record<string, string>): string {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const BOOTSTRAP_REQUIRED_TEMPLATE_FILES = [
|
||||
'users.csv',
|
||||
'organizations.csv',
|
||||
'memberships.csv',
|
||||
'crm-role-assignments.csv',
|
||||
'master-options.csv',
|
||||
'branches.csv',
|
||||
'product-types.csv',
|
||||
@@ -34,6 +35,96 @@ export const BOOTSTRAP_OPTIONAL_TEMPLATE_FILES = [
|
||||
'quotation-topic-items.csv'
|
||||
] as const;
|
||||
|
||||
export const BOOTSTRAP_REQUIRED_STEPS = [
|
||||
{
|
||||
stepKey: 'bootstrap_users',
|
||||
fileName: 'users.csv',
|
||||
title: 'Users',
|
||||
description: 'Create the setup administrator and required app users.',
|
||||
dependsOn: []
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_organizations',
|
||||
fileName: 'organizations.csv',
|
||||
title: 'Organizations',
|
||||
description: 'Create tenant organization records after users exist.',
|
||||
dependsOn: ['bootstrap_users']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_memberships',
|
||||
fileName: 'memberships.csv',
|
||||
title: 'Memberships',
|
||||
description: 'Connect users to organizations with app-level roles.',
|
||||
dependsOn: ['bootstrap_users', 'bootstrap_organizations']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_crm_role_assignments',
|
||||
fileName: 'crm-role-assignments.csv',
|
||||
title: 'CRM Roles',
|
||||
description: 'Assign CRM role profiles and branch/product scopes.',
|
||||
dependsOn: ['bootstrap_users', 'bootstrap_organizations', 'bootstrap_memberships']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_master_options',
|
||||
fileName: 'master-options.csv',
|
||||
title: 'Master Options',
|
||||
description: 'Seed governed CRM option categories and labels.',
|
||||
dependsOn: ['bootstrap_users', 'bootstrap_organizations', 'bootstrap_memberships']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_branches',
|
||||
fileName: 'branches.csv',
|
||||
title: 'Branches',
|
||||
description: 'Create branch options used for CRM scope and numbering.',
|
||||
dependsOn: [
|
||||
'bootstrap_users',
|
||||
'bootstrap_organizations',
|
||||
'bootstrap_memberships',
|
||||
'bootstrap_master_options'
|
||||
]
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_product_types',
|
||||
fileName: 'product-types.csv',
|
||||
title: 'Product Types',
|
||||
description: 'Create product type options used for CRM scope and numbering.',
|
||||
dependsOn: [
|
||||
'bootstrap_users',
|
||||
'bootstrap_organizations',
|
||||
'bootstrap_memberships',
|
||||
'bootstrap_master_options'
|
||||
]
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_document_sequences',
|
||||
fileName: 'document-sequences.csv',
|
||||
title: 'Document Sequences',
|
||||
description: 'Configure document numbering for CRM entities.',
|
||||
dependsOn: ['bootstrap_organizations', 'bootstrap_branches', 'bootstrap_product_types']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_approval_workflows',
|
||||
fileName: 'approval-workflows.csv',
|
||||
title: 'Approval Workflows',
|
||||
description: 'Create approval workflow headers.',
|
||||
dependsOn: ['bootstrap_organizations']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_approval_steps',
|
||||
fileName: 'approval-steps.csv',
|
||||
title: 'Approval Steps',
|
||||
description: 'Create ordered approval steps for each workflow.',
|
||||
dependsOn: ['bootstrap_approval_workflows']
|
||||
},
|
||||
{
|
||||
stepKey: 'bootstrap_approval_matrix',
|
||||
fileName: 'approval-matrix.csv',
|
||||
title: 'Approval Matrix',
|
||||
description: 'Create approval routing rules by scope and amount.',
|
||||
dependsOn: ['bootstrap_approval_workflows', 'bootstrap_branches', 'bootstrap_product_types']
|
||||
}
|
||||
] as const;
|
||||
|
||||
export class BootstrapSetupError extends Error {
|
||||
status: number;
|
||||
details?: Record<string, unknown>;
|
||||
@@ -57,11 +148,16 @@ export interface BootstrapSetupStatus {
|
||||
};
|
||||
requiredFiles: string[];
|
||||
optionalFiles: string[];
|
||||
requiredSteps: typeof BOOTSTRAP_REQUIRED_STEPS;
|
||||
}
|
||||
|
||||
async function hasRows<T>(query: Promise<Array<T & { value: number }>>) {
|
||||
try {
|
||||
const [row] = await query;
|
||||
return Number(row?.value ?? 0) > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatus> {
|
||||
@@ -99,7 +195,8 @@ export async function getBootstrapSetupStatus(): Promise<BootstrapSetupStatus> {
|
||||
tokenRequired: Boolean(process.env.BOOTSTRAP_SETUP_TOKEN?.trim()),
|
||||
checks,
|
||||
requiredFiles: [...BOOTSTRAP_REQUIRED_TEMPLATE_FILES],
|
||||
optionalFiles: [...BOOTSTRAP_OPTIONAL_TEMPLATE_FILES]
|
||||
optionalFiles: [...BOOTSTRAP_OPTIONAL_TEMPLATE_FILES],
|
||||
requiredSteps: [...BOOTSTRAP_REQUIRED_STEPS]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,7 +223,12 @@ export function assertBootstrapToken(token: string | null | undefined) {
|
||||
}
|
||||
|
||||
export function getMissingBootstrapRequiredFiles(files: CsvPreviewFileInput[]) {
|
||||
const uploaded = new Set(files.map((file) => file.fileName.trim().toLowerCase()));
|
||||
const uploaded = new Set(
|
||||
files
|
||||
.map((file) => file.fileName)
|
||||
.filter((fileName): fileName is string => typeof fileName === 'string')
|
||||
.map((fileName) => fileName.trim().toLowerCase())
|
||||
);
|
||||
return BOOTSTRAP_REQUIRED_TEMPLATE_FILES.filter((fileName) => !uploaded.has(fileName));
|
||||
}
|
||||
|
||||
@@ -140,6 +242,37 @@ export function assertBootstrapRequiredFiles(files: CsvPreviewFileInput[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function assertBootstrapStepFiles(stepKey: string, files: CsvPreviewFileInput[]) {
|
||||
const step = BOOTSTRAP_REQUIRED_STEPS.find((candidate) => candidate.stepKey === stepKey);
|
||||
|
||||
if (!step) {
|
||||
throw new BootstrapSetupError('Unknown bootstrap setup step.', 400, {
|
||||
stepKey,
|
||||
allowedStepKeys: BOOTSTRAP_REQUIRED_STEPS.map((candidate) => candidate.stepKey)
|
||||
});
|
||||
}
|
||||
|
||||
if (files.length !== 1) {
|
||||
throw new BootstrapSetupError('Upload exactly one CSV file for this setup step.', 400, {
|
||||
stepKey,
|
||||
expectedFileName: step.fileName,
|
||||
receivedFiles: files.map((file) => file.fileName)
|
||||
});
|
||||
}
|
||||
|
||||
const [file] = files;
|
||||
|
||||
if (file.fileName.toLowerCase() !== step.fileName.toLowerCase()) {
|
||||
throw new BootstrapSetupError('Uploaded CSV file does not match the current setup step.', 400, {
|
||||
stepKey,
|
||||
expectedFileName: step.fileName,
|
||||
receivedFileName: file.fileName
|
||||
});
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
export function setupStepStatusFromReportStatus(status: 'PASS' | 'WARNING' | 'FAIL'): SetupStepStatus {
|
||||
if (status === 'PASS') return 'passed';
|
||||
if (status === 'WARNING') return 'warning';
|
||||
|
||||
@@ -77,7 +77,46 @@ const STALE_DEPENDENCIES: Record<string, string[]> = {
|
||||
sequences: ['verification'],
|
||||
approval: ['csv_preview', 'csv_commit', 'verification'],
|
||||
csv_preview: ['csv_commit'],
|
||||
csv_commit: ['verification']
|
||||
csv_commit: ['verification'],
|
||||
bootstrap_users: [
|
||||
'bootstrap_organizations',
|
||||
'bootstrap_memberships',
|
||||
'bootstrap_crm_role_assignments',
|
||||
'bootstrap_master_options',
|
||||
'bootstrap_branches',
|
||||
'bootstrap_product_types',
|
||||
'bootstrap_document_sequences',
|
||||
'bootstrap_approval_workflows',
|
||||
'bootstrap_approval_steps',
|
||||
'bootstrap_approval_matrix'
|
||||
],
|
||||
bootstrap_organizations: [
|
||||
'bootstrap_memberships',
|
||||
'bootstrap_crm_role_assignments',
|
||||
'bootstrap_master_options',
|
||||
'bootstrap_branches',
|
||||
'bootstrap_product_types',
|
||||
'bootstrap_document_sequences',
|
||||
'bootstrap_approval_workflows',
|
||||
'bootstrap_approval_steps',
|
||||
'bootstrap_approval_matrix'
|
||||
],
|
||||
bootstrap_memberships: [
|
||||
'bootstrap_crm_role_assignments',
|
||||
'bootstrap_master_options',
|
||||
'bootstrap_branches',
|
||||
'bootstrap_product_types',
|
||||
'bootstrap_document_sequences'
|
||||
],
|
||||
bootstrap_master_options: [
|
||||
'bootstrap_branches',
|
||||
'bootstrap_product_types',
|
||||
'bootstrap_document_sequences',
|
||||
'bootstrap_approval_matrix'
|
||||
],
|
||||
bootstrap_branches: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
|
||||
bootstrap_product_types: ['bootstrap_document_sequences', 'bootstrap_approval_matrix'],
|
||||
bootstrap_approval_workflows: ['bootstrap_approval_steps', 'bootstrap_approval_matrix']
|
||||
};
|
||||
|
||||
function now() {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface BootstrapStatusResponse {
|
||||
function isBootstrapAllowedPath(pathname: string) {
|
||||
return (
|
||||
pathname === '/administration/setup' ||
|
||||
pathname.startsWith('/api/auth/') ||
|
||||
pathname.startsWith('/api/setup/bootstrap/') ||
|
||||
pathname === '/api/setup/templates'
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@import './themes/zen.css';
|
||||
@import './themes/astro-vista.css';
|
||||
@import './themes/whatsapp.css';
|
||||
@import './themes/alla.css';
|
||||
|
||||
body {
|
||||
@apply overscroll-none;
|
||||
|
||||
117
src/styles/themes/alla.css
Normal file
117
src/styles/themes/alla.css
Normal file
@@ -0,0 +1,117 @@
|
||||
[data-theme='alla'] {
|
||||
--background: oklch(0.9911 0 0);
|
||||
--foreground: oklch(0.2046 0 0);
|
||||
--card: oklch(0.9911 0 0);
|
||||
--card-foreground: oklch(0.2046 0 0);
|
||||
--popover: oklch(0.9911 0 0);
|
||||
--popover-foreground: oklch(0.4386 0 0);
|
||||
--primary: oklch(0.4799 0.0804 155.8197);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.994 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9461 0 0);
|
||||
--muted-foreground: oklch(0.2435 0 0);
|
||||
--accent: oklch(0.9461 0 0);
|
||||
--accent-foreground: oklch(0.2435 0 0);
|
||||
--destructive: oklch(0.5523 0.1927 32.7272);
|
||||
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||
--border: oklch(0.9037 0 0);
|
||||
--input: oklch(0.9731 0 0);
|
||||
--ring: oklch(0.5568 0.1355 155.812);
|
||||
--chart-1: oklch(0.8348 0.1302 160.908);
|
||||
--chart-2: oklch(0.6231 0.188 259.8145);
|
||||
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||
--sidebar: oklch(0.9911 0 0);
|
||||
--sidebar-foreground: oklch(0.5452 0 0);
|
||||
--sidebar-primary: oklch(0.5568 0.1355 155.812);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.9461 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||
--sidebar-border: oklch(0.9037 0 0);
|
||||
--sidebar-ring: oklch(0.5568 0.1355 155.812);
|
||||
--font-sans: Anek Kannada, ui-sans-serif, sans-serif, system-ui;
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: monospace;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme='alla'].dark {
|
||||
--background: oklch(0.1822 0 0);
|
||||
--foreground: oklch(0.9851 0 0);
|
||||
--card: oklch(0.2046 0 0);
|
||||
--card-foreground: oklch(0.9851 0 0);
|
||||
--popover: oklch(0.2046 0 0);
|
||||
--popover-foreground: oklch(0.9851 0 0);
|
||||
--primary: oklch(0.5568 0.1355 155.812);
|
||||
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--secondary: oklch(0.2603 0 0);
|
||||
--secondary-foreground: oklch(0.9851 0 0);
|
||||
--muted: oklch(0.2603 0 0);
|
||||
--muted-foreground: oklch(0.7122 0 0);
|
||||
--accent: oklch(0.3132 0 0);
|
||||
--accent-foreground: oklch(0.9851 0 0);
|
||||
--destructive: oklch(0.3123 0.0852 29.7877);
|
||||
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||
--border: oklch(0.2809 0 0);
|
||||
--input: oklch(0.2603 0 0);
|
||||
--ring: oklch(0.5568 0.1355 155.812);
|
||||
--chart-1: oklch(0.8003 0.1821 151.711);
|
||||
--chart-2: oklch(0.7137 0.1434 254.624);
|
||||
--chart-3: oklch(0.709 0.1592 293.5412);
|
||||
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||
--chart-5: oklch(0.7845 0.1325 181.912);
|
||||
--sidebar: oklch(0.1822 0 0);
|
||||
--sidebar-foreground: oklch(0.6301 0 0);
|
||||
--sidebar-primary: oklch(0.5568 0.1355 155.812);
|
||||
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--sidebar-accent: oklch(0.3132 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-border: oklch(0.2809 0 0);
|
||||
--sidebar-ring: oklch(0.5568 0.1355 155.812);
|
||||
}
|
||||
|
||||
[data-theme='alla'] {
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user