Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3ebf44c
feat: introduce demo template system with modular seeding and industr…
notbokuto Dec 17, 2025
d73f1a1
- added seed demo data and new models
notbokuto Jan 6, 2026
e98e2c7
- delete old files
notbokuto Jan 6, 2026
7dabc9c
- moved seeding functions from RPCs to code
notbokuto Jan 7, 2026
dea1bd9
- new cards for industry
notbokuto Jan 7, 2026
c761a40
- added UI changes
notbokuto Jan 7, 2026
8de943b
- added delete demo data UI
notbokuto Jan 9, 2026
197de49
- some clenup
notbokuto Jan 9, 2026
77746ba
- removed unused code
notbokuto Jan 9, 2026
1250b25
- comment fixes
notbokuto Jan 9, 2026
a0f9291
- delete unused file
notbokuto Jan 9, 2026
ac83a34
- Moved industry to last step
jeebeez Jan 12, 2026
a9a9fa7
- claude rework and cleanup
jeebeez Jan 12, 2026
bfe0ed0
style: ask whether they want demo data before asking which industry i…
barbinbrad Jan 13, 2026
0f56284
build: regenerate types and package-lock.json
barbinbrad Jan 13, 2026
7feadb6
- workcenters and make methods added
jeebeez Jan 14, 2026
71f817d
Merge branch 'main' into feat/add-support-for-demo-data
jeebeez Jan 15, 2026
e8de928
feat: Remove the 'Other' custom industry option from onboarding and s…
jeebeez Jan 15, 2026
7f67c89
refactor: update and generalize demo industry names and descriptions …
jeebeez Jan 15, 2026
8d756e5
Reduced items, fixed bugs with BOM and make types, final changes
jeebeez Jan 20, 2026
9bbfbd5
Merge branch 'main' into feat/add-support-for-demo-data
jeebeez Jan 30, 2026
48ea74d
swagger changes
jeebeez Jan 30, 2026
1854a72
- added training modules to paths
jeebeez Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/erp/app/components/Form/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Radios } from "@carbon/form";

type RadioGroupProps = {
name: string;
label?: string;
options: { label: string; value: string; description?: string }[];
orientation?: "horizontal" | "vertical";
};

const RadioGroup = ({
name,
label,
options,
orientation = "vertical"
}: RadioGroupProps) => {
// Filter out description field as Radios doesn't support it
const radioOptions = options.map(({ label, value }) => ({ label, value }));

return (
<Radios
name={name}
label={label}
options={radioOptions}
orientation={orientation}
/>
);
};

export default RadioGroup;
3 changes: 2 additions & 1 deletion apps/erp/app/components/Form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
TimePicker,
Timezone
} from "@carbon/form";

import Abilities from "./Abilities";
import Ability from "./Ability";
import Account from "./Account";
Expand Down Expand Up @@ -57,6 +56,7 @@ import Part from "./Part";
import PaymentTerm from "./PaymentTerm";
import Process from "./Process";
import Processes from "./Processes";
import RadioGroup from "./RadioGroup";
import Sequence from "./Sequence";
import SequenceOrCustomId from "./SequenceOrCustomId";
import Service from "./Service";
Expand Down Expand Up @@ -128,6 +128,7 @@ export {
PhoneInput,
Process,
Processes,
RadioGroup,
Radios,
Select,
SelectControlled,
Expand Down
87 changes: 87 additions & 0 deletions apps/erp/app/components/TrainingPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Button, IconButton } from "@carbon/react";
import { AnimatePresence, motion } from "framer-motion";
import { LuExternalLink, LuX } from "react-icons/lu";
import type { TrainingVideo } from "~/utils/training";
import { getVideoEmbedUrl } from "~/utils/training";

type TrainingPanelProps = {
training: TrainingVideo | null;
isOpen: boolean;
onDismiss: () => void;
};

export default function TrainingPanel({
training,
isOpen,
onDismiss
}: TrainingPanelProps) {
if (!training) return null;

const embedUrl = getVideoEmbedUrl(training.videoUrl, training.videoType);

return (
<AnimatePresence mode="wait">
{isOpen && (
<motion.div
key={training.title}
initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, y: 10, scale: 0.95, filter: "blur(4px)" }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="fixed bottom-4 right-4 w-[380px] rounded-xl border bg-background shadow-lg z-40 overflow-hidden"
>
<div className="relative aspect-video w-full bg-muted">
<iframe
src={embedUrl}
title={training.title}
className="w-full h-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-background to-transparent pointer-events-none" />
<IconButton
aria-label="Close"
icon={<LuX />}
variant="ghost"
size="sm"
className="absolute top-2 right-2 bg-background/80 backdrop-blur-sm hover:bg-background"
onClick={onDismiss}
/>
</div>

<div className="px-4 pt-3.5 pb-5 space-y-1">
<h3 className="text-sm font-semibold tracking-tight">
{training.title}
</h3>
<p className="text-xs text-muted-foreground">
{training.description}
</p>
</div>

<div className="px-4 pb-3.5 flex items-center justify-end gap-2">
<Button variant="secondary" size="sm" onClick={onDismiss}>
Dismiss
</Button>
{training.academyUrl ? (
<Button
size="sm"
rightIcon={<LuExternalLink />}
onClick={() => window.open(training.academyUrl, "_blank")}
>
View in Academy
</Button>
) : (
<Button
size="sm"
rightIcon={<LuExternalLink />}
onClick={() => window.open(training.videoUrl, "_blank")}
>
Watch full video
</Button>
)}
</div>
</motion.div>
)}
</AnimatePresence>
);
}
2 changes: 2 additions & 0 deletions apps/erp/app/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { usePercentFormatter } from "./usePercentFormatter";
import { usePermissions } from "./usePermissions";
import { useRealtime } from "./useRealtime";
import { useScrollPosition } from "./useScrollPosition";
import { useTrainingPanel } from "./useTrainingPanel";
import { useUser } from "./useUser";

export {
Expand All @@ -33,6 +34,7 @@ export {
useRealtime,
useRouteData,
useScrollPosition,
useTrainingPanel,
useUrlParams,
useUser
};
43 changes: 43 additions & 0 deletions apps/erp/app/hooks/useTrainingPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMemo } from "react";
import { useFetcher, useLocation } from "react-router";
import { path } from "~/utils/path";
import { getTrainingForPath, getTrainingKey } from "~/utils/training";
import { useUser } from "./useUser";

const FLAG_PREFIX = "training:";

export function useTrainingPanel() {
const { pathname } = useLocation();
const { flags } = useUser();
const fetcher = useFetcher({ key: "training-dismiss" });

const training = useMemo(() => getTrainingForPath(pathname), [pathname]);
const trainingKey = useMemo(() => getTrainingKey(pathname), [pathname]);

const flagKey = trainingKey ? `${FLAG_PREFIX}${trainingKey}` : null;

// Optimistic: check if there's a pending dismiss for THIS flag
const pendingDismissFlag = fetcher.formData?.get("flag") as string | null;
const isPendingDismiss = pendingDismissFlag === flagKey;

const isDismissed = flagKey
? isPendingDismiss || flags[flagKey] === true
: false;

const isOpen = !!training && !!flagKey && !isDismissed;

const dismiss = () => {
if (!flagKey) return;
fetcher.submit(
{ intent: "flag", flag: flagKey, value: "true" },
{ method: "POST", action: path.to.acknowledge }
);
};

return {
isOpen,
training,
hasTraining: training !== null,
dismiss
};
}
6 changes: 5 additions & 1 deletion apps/erp/app/hooks/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ type Defaults = {
locationId: string | null;
};

type UserFlags = Record<string, boolean>;

type User = PersonalData & {
company: Company;
groups: Groups;
defaults: Defaults;
flags: UserFlags;
};

export function useUser(): User {
Expand All @@ -55,7 +58,8 @@ export function useUser(): User {
...data.user,
company: data.company,
groups: data.groups,
defaults: data.defaults ?? { locationId: null }
defaults: data.defaults ?? { locationId: null },
flags: (data.user.flags as UserFlags) ?? {}
};
}

Expand Down
26 changes: 23 additions & 3 deletions apps/erp/app/modules/settings/settings.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,40 @@ export const apiKeyValidator = z.object({
name: z.string().min(1, { message: "Name is required" })
});

const company = {
export const onboardingIndustryTypes = [
"robotics_oem",
"cnc_aerospace",
"metal_fabrication",
"automotive_precision",
"custom"
] as const;

const companyAddress = {
name: z.string().min(1, { message: "Name is required" }),
taxId: zfd.text(z.string().optional()),
addressLine1: z.string().min(1, { message: "Address is required" }),
addressLine2: zfd.text(z.string().optional()),
city: z.string().min(1, { message: "City is required" }),
stateProvince: z.string().min(1, { message: "State / Province is required" }),
postalCode: z.string().min(1, { message: "Postal Code is required" }),
countryCode: z.string().min(1, { message: "Country is required" }),
baseCurrencyCode: zfd.text(z.string()),
website: zfd.text(z.string().optional())
};

export const addressValidator = z.object({
...companyAddress,
next: z.string().min(1, { message: "Next is required" })
});

const company = {
...companyAddress,
taxId: zfd.text(z.string().optional()),
phone: zfd.text(z.string().optional()),
fax: zfd.text(z.string().optional()),
email: zfd.text(z.string().optional()),
website: zfd.text(z.string().optional())
industryId: z.enum(onboardingIndustryTypes).optional().default("custom"),
customIndustryDescription: z.string().optional(),
seedDemoData: zfd.checkbox()
};

export const companyValidator = z.object(company);
Expand Down
7 changes: 7 additions & 0 deletions apps/erp/app/modules/settings/ui/useSettingsSubmodules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
LuClipboardCheck,
LuCreditCard,
LuCrown,
LuDatabase,
LuFactory,
LuImage,
LuLayoutDashboard,
Expand Down Expand Up @@ -52,6 +53,12 @@ const settingsRoutes: AuthenticatedRouteGroup<{
to: path.to.logos,
role: "employee",
icon: <LuImage />
},
{
name: "Demo Data",
to: path.to.demoData,
role: "employee",
icon: <LuDatabase />
}
]
},
Expand Down
Loading