uat-seed-script
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
'use client';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
"use client";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -7,8 +11,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -22,18 +26,18 @@ import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail
|
||||
} from '@/components/ui/sidebar';
|
||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
||||
import { navGroups } from '@/config/nav-config';
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { useFilteredNavGroups } from '@/hooks/use-nav';
|
||||
import Link from 'next/link';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { Icons } from '../icons';
|
||||
import { OrgSwitcher } from '../org-switcher';
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||
import { navGroups } from "@/config/nav-config";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useFilteredNavGroups } from "@/hooks/use-nav";
|
||||
import Link from "next/link";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { Icons } from "../icons";
|
||||
import { OrgSwitcher } from "../org-switcher";
|
||||
|
||||
export default function AppSidebar() {
|
||||
const pathname = usePathname();
|
||||
@@ -44,59 +48,67 @@ export default function AppSidebar() {
|
||||
const user = session?.user ?? null;
|
||||
const organization = session?.user?.activeOrganizationId;
|
||||
const canManageOrganization =
|
||||
session?.user?.systemRole === 'super_admin' ||
|
||||
(!!organization && session?.user?.activeMembershipRole === 'admin');
|
||||
session?.user?.systemRole === "super_admin" ||
|
||||
(!!organization && session?.user?.activeMembershipRole === "admin");
|
||||
|
||||
React.useEffect(() => {
|
||||
// Side effects based on sidebar state changes
|
||||
}, [isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('[sidebar-debug]', {
|
||||
console.info("[sidebar-debug]", {
|
||||
systemRole: session?.user?.systemRole ?? null,
|
||||
activeOrganizationId: session?.user?.activeOrganizationId ?? null,
|
||||
activeMembershipRole: session?.user?.activeMembershipRole ?? null,
|
||||
activePermissions: session?.user?.activePermissions ?? [],
|
||||
visibleItems: filteredGroups.flatMap((group) => group.items.map((item) => item.title))
|
||||
visibleItems: filteredGroups.flatMap((group) =>
|
||||
group.items.map((item) => item.title),
|
||||
),
|
||||
});
|
||||
}, [
|
||||
filteredGroups,
|
||||
session?.user?.activeOrganizationId,
|
||||
session?.user?.activePermissions,
|
||||
session?.user?.activeMembershipRole,
|
||||
session?.user?.systemRole
|
||||
session?.user?.systemRole,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Sidebar collapsible='icon'>
|
||||
<SidebarHeader className='group-data-[collapsible=icon]:pt-4'>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="group-data-[collapsible=icon]:pt-4">
|
||||
<OrgSwitcher />
|
||||
</SidebarHeader>
|
||||
<SidebarContent className='overflow-x-hidden'>
|
||||
<SidebarContent className="overflow-x-hidden">
|
||||
{filteredGroups.map((group) => (
|
||||
<SidebarGroup key={group.label || 'ungrouped'} className='py-0'>
|
||||
{group.label && <SidebarGroupLabel>{group.label}</SidebarGroupLabel>}
|
||||
<SidebarGroup key={group.label || "ungrouped"} className="py-0">
|
||||
{group.label && (
|
||||
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
||||
)}
|
||||
<SidebarMenu>
|
||||
{group.items.map((item) => {
|
||||
const Icon = item.icon ? Icons[item.icon] : Icons.logo;
|
||||
const itemKey = item.url || `${group.label ?? 'group'}-${item.title}`;
|
||||
const itemKey =
|
||||
item.url || `${group.label ?? "group"}-${item.title}`;
|
||||
return item?.items && item?.items?.length > 0 ? (
|
||||
<Collapsible
|
||||
key={itemKey}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className='group/collapsible'
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title} isActive={pathname === item.url}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={pathname === item.url}
|
||||
>
|
||||
{item.icon && <Icon />}
|
||||
<span>{item.title}</span>
|
||||
<Icons.chevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
||||
<Icons.chevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
@@ -105,7 +117,10 @@ export default function AppSidebar() {
|
||||
<SidebarMenuSubItem
|
||||
key={subItem.url || `${itemKey}-${subItem.title}`}
|
||||
>
|
||||
<SidebarMenuSubButton asChild isActive={pathname === subItem.url}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={pathname === subItem.url}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
@@ -141,49 +156,57 @@ export default function AppSidebar() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
{user && (
|
||||
<UserAvatarProfile className='h-8 w-8 rounded-lg' showInfo user={user} />
|
||||
<UserAvatarProfile
|
||||
className="h-8 w-8 rounded-lg"
|
||||
showInfo
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<Icons.chevronsDown className='ml-auto size-4' />
|
||||
<Icons.chevronsDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
||||
side='bottom'
|
||||
align='end'
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='p-0 font-normal'>
|
||||
<div className='px-1 py-1.5'>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="px-1 py-1.5">
|
||||
{user && (
|
||||
<UserAvatarProfile className='h-8 w-8 rounded-lg' showInfo user={user} />
|
||||
<UserAvatarProfile
|
||||
className="h-8 w-8 rounded-lg"
|
||||
showInfo
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
||||
<Icons.account className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/dashboard/profile")}
|
||||
>
|
||||
<Icons.account className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
{canManageOrganization && (
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/billing')}>
|
||||
<Icons.creditCard className='mr-2 h-4 w-4' />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/notifications')}>
|
||||
<Icons.notification className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/dashboard/notifications")}
|
||||
>
|
||||
<Icons.notification className="mr-2 h-4 w-4" />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
||||
<Icons.logout className='mr-2 h-4 w-4' />
|
||||
<DropdownMenuItem
|
||||
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||
>
|
||||
<Icons.logout className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { UserAvatarProfile } from '@/components/user-avatar-profile';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { UserAvatarProfile } from "@/components/user-avatar-profile";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
export function UserNav() {
|
||||
const { data: session } = useSession();
|
||||
const user = session?.user;
|
||||
@@ -21,28 +21,37 @@ export function UserNav() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<UserAvatarProfile user={user} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='w-56' align='end' sideOffset={10} forceMount>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<p className='text-sm leading-none font-medium'>{user.name}</p>
|
||||
<p className='text-muted-foreground text-xs leading-none'>{user.email}</p>
|
||||
<DropdownMenuContent
|
||||
className="w-56"
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
forceMount
|
||||
>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">{user.name}</p>
|
||||
<p className="text-muted-foreground text-xs leading-none">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
|
||||
<DropdownMenuItem onClick={() => router.push("/dashboard/profile")}>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>Billing</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => void signOut({ redirectTo: '/auth/sign-in' })}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void signOut({ redirectTo: "/auth/sign-in" })}
|
||||
>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -345,6 +345,7 @@ type UserContext = {
|
||||
admin: UserMembershipRow;
|
||||
marketing: UserMembershipRow;
|
||||
sales: UserMembershipRow[];
|
||||
salesByProductType: Partial<Record<string, UserMembershipRow>>;
|
||||
salesManager: UserMembershipRow;
|
||||
departmentManager: UserMembershipRow;
|
||||
topManager: UserMembershipRow;
|
||||
@@ -352,33 +353,41 @@ type UserContext = {
|
||||
|
||||
const SEED_TAG = "crm-uat-seed-v1";
|
||||
const PERIOD = "2606";
|
||||
const UAT_EMAILS = {
|
||||
marketing: "mk.uat@alla.local",
|
||||
salesCrane: "sales.crane.uat@alla.local",
|
||||
salesDockDoor: "sales.dockdoor.uat@alla.local",
|
||||
salesManager: "mgr.sales.uat@alla.local",
|
||||
topManager: "ceo.uat@alla.local",
|
||||
admin: "admin.uat@alla.local",
|
||||
} as const;
|
||||
|
||||
const STORIES: StoryConfig[] = [
|
||||
{
|
||||
slug: "website-warehouse-crane-win",
|
||||
title: "Lead received from website and won after approval",
|
||||
title: "Siam Manufacturing crane project waiting final CEO approval",
|
||||
customerName: "บริษัท บางกอก ฟู้ด อินดัสทรี จำกัด",
|
||||
province: "Bangkok",
|
||||
district: "Min Buri",
|
||||
province: "Chachoengsao",
|
||||
district: "Bang Pakong",
|
||||
branchCode: "head_office",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "factory",
|
||||
productTypeCode: "crane",
|
||||
quotationTypeCode: "crane",
|
||||
priorityCode: "high",
|
||||
leadChannelCode: "website",
|
||||
leadChannelCode: "referral",
|
||||
awarenessCode: "google_search_website",
|
||||
leadStatusCode: "assigned",
|
||||
leadFollowupStatusCode: "waiting_po",
|
||||
leadFollowupStatusCode: "drawing_followup",
|
||||
opportunityStatusCode: "negotiation",
|
||||
chancePercent: 80,
|
||||
estimatedValue: 12400000,
|
||||
projectName: "Warehouse Crane Installation Phase 2",
|
||||
projectLocation: "Bangkok Food Industrial Estate, Min Buri",
|
||||
chancePercent: 78,
|
||||
estimatedValue: 8750000,
|
||||
projectName: "Overhead Crane Installation Phase 1",
|
||||
projectLocation: "Toyota Gateway Plant",
|
||||
leadDescription:
|
||||
"Customer asked for top-running overhead crane to support raw material warehouse expansion.",
|
||||
quotePlan: "accepted",
|
||||
opportunityOutcome: "won",
|
||||
"Marketing qualified the crane project and handed it to the crane sales queue for quotation approval.",
|
||||
quotePlan: "pending_step_2",
|
||||
opportunityOutcome: "open",
|
||||
multiParties: [
|
||||
{
|
||||
roleCode: "contractor",
|
||||
@@ -392,57 +401,57 @@ const STORIES: StoryConfig[] = [
|
||||
},
|
||||
{
|
||||
slug: "facebook-lost-price",
|
||||
title: "Lead lost to competitor because price too high",
|
||||
title: "Demo lost customer with no quotation because budget not approved",
|
||||
customerName: "บริษัท ไทย ออโต้ เพรส จำกัด",
|
||||
province: "Samut Prakan",
|
||||
district: "Bang Phli",
|
||||
branchCode: "factory",
|
||||
province: "Bangkok",
|
||||
district: "Lat Krabang",
|
||||
branchCode: "service",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "factory",
|
||||
productTypeCode: "crane",
|
||||
quotationTypeCode: "crane",
|
||||
customerSubGroupCode: "warehouse",
|
||||
productTypeCode: "dockdoor",
|
||||
quotationTypeCode: "dockdoor",
|
||||
priorityCode: "normal",
|
||||
leadChannelCode: "facebook",
|
||||
awarenessCode: "social_media",
|
||||
leadStatusCode: "closed_lost",
|
||||
leadFollowupStatusCode: "quotation_preparation",
|
||||
opportunityStatusCode: "quotation_created",
|
||||
chancePercent: 45,
|
||||
estimatedValue: 5800000,
|
||||
projectName: "Stamping Line Hoist Replacement",
|
||||
projectLocation: "Bang Phli Industrial Estate",
|
||||
leadChannelCode: "website",
|
||||
awarenessCode: "google_search_website",
|
||||
leadStatusCode: "cancel",
|
||||
leadFollowupStatusCode: "budget_pending",
|
||||
opportunityStatusCode: "qualification",
|
||||
chancePercent: 20,
|
||||
estimatedValue: 1900000,
|
||||
projectName: "Budget Hold Demo Project",
|
||||
projectLocation: "Lat Krabang Logistics Zone",
|
||||
leadDescription:
|
||||
"Customer compared 5 ton monorail hoist replacement between ALLA and Konecranes.",
|
||||
competitor: "Konecranes",
|
||||
quotePlan: "sent_follow_up",
|
||||
opportunityOutcome: "lost",
|
||||
lostReasonCode: "price_too_high",
|
||||
"Opportunity was stopped before quotation because customer budget was not approved and competitor was still under consideration.",
|
||||
competitor: "Competitor Selected",
|
||||
quotePlan: "cancelled",
|
||||
opportunityOutcome: "no_quotation",
|
||||
noQuotationReasonCode: "budget_not_ready",
|
||||
},
|
||||
{
|
||||
slug: "revision-r02-approved",
|
||||
title: "Customer requested revision and approved revision R02",
|
||||
title: "Eastern Logistics dock door project in draft and follow-up stage",
|
||||
customerName: "บริษัท กรีน โลจิสติกส์ พาร์ค จำกัด",
|
||||
province: "Pathum Thani",
|
||||
district: "Khlong Luang",
|
||||
province: "Chonburi",
|
||||
district: "Si Racha",
|
||||
branchCode: "service",
|
||||
customerGroupCode: "industrial",
|
||||
customerSubGroupCode: "logistics",
|
||||
customerSubGroupCode: "warehouse",
|
||||
productTypeCode: "dockdoor",
|
||||
quotationTypeCode: "dockdoor",
|
||||
priorityCode: "high",
|
||||
leadChannelCode: "referral",
|
||||
awarenessCode: "word_of_mouth",
|
||||
leadStatusCode: "assigned",
|
||||
leadFollowupStatusCode: "waiting_po",
|
||||
opportunityStatusCode: "negotiation",
|
||||
chancePercent: 70,
|
||||
estimatedValue: 9300000,
|
||||
projectName: "Cold Chain Dock Leveller Upgrade",
|
||||
projectLocation: "Khlong Luang Logistics Campus",
|
||||
leadFollowupStatusCode: "quotation_preparation",
|
||||
opportunityStatusCode: "proposal_preparation",
|
||||
chancePercent: 61,
|
||||
estimatedValue: 4600000,
|
||||
projectName: "Loading Dock Expansion",
|
||||
projectLocation: "WHA Warehouse",
|
||||
leadDescription:
|
||||
"Customer requested revision after consultant changed dock layout and bumper specification.",
|
||||
quotePlan: "rejected_then_revised",
|
||||
opportunityOutcome: "won",
|
||||
"Dock door salesperson is still following up layout confirmation before final quotation submission.",
|
||||
quotePlan: "sent_follow_up",
|
||||
opportunityOutcome: "open",
|
||||
multiParties: [
|
||||
{
|
||||
roleCode: "consultant",
|
||||
@@ -882,6 +891,30 @@ function buildDocumentCode(prefix: string, number: number) {
|
||||
return `${prefix}${PERIOD}-${String(number).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
function buildQuotationCode(productTypeCode: string, number: number) {
|
||||
const prefixByProductType: Record<string, string> = {
|
||||
crane: "CR",
|
||||
dockdoor: "DK",
|
||||
solarcell: "SOL",
|
||||
service: "SV",
|
||||
spare_part: "SP",
|
||||
};
|
||||
|
||||
return buildDocumentCode(prefixByProductType[productTypeCode] ?? "QT", number);
|
||||
}
|
||||
|
||||
function pickSalesUser(
|
||||
users: UserContext,
|
||||
productTypeCode: string | undefined,
|
||||
index: number
|
||||
) {
|
||||
if (productTypeCode && users.salesByProductType[productTypeCode]) {
|
||||
return users.salesByProductType[productTypeCode]!;
|
||||
}
|
||||
|
||||
return choose(users.sales, index);
|
||||
}
|
||||
|
||||
function seededNote(scope: string) {
|
||||
return `[${SEED_TAG}:${scope}]`;
|
||||
}
|
||||
@@ -1027,21 +1060,48 @@ function buildUserContext(rows: UserMembershipRow[]): UserContext {
|
||||
}
|
||||
|
||||
const first = rows[0];
|
||||
const byRole = (role: string) =>
|
||||
rows.find((row) => row.businessRole === role) ?? null;
|
||||
const salesRows = rows.filter((row) => row.businessRole === "sales");
|
||||
const byRole = (role: string) => rows.find((row) => row.businessRole === role) ?? null;
|
||||
const byEmail = (email: string) =>
|
||||
rows.find((row) => row.email.toLowerCase() === email.toLowerCase()) ?? null;
|
||||
const fallbackSalesRows = rows.filter((row) => row.businessRole === "sales");
|
||||
const preferredSalesRows = [
|
||||
byEmail(UAT_EMAILS.salesCrane),
|
||||
byEmail(UAT_EMAILS.salesDockDoor),
|
||||
].filter((row): row is UserMembershipRow => row !== null);
|
||||
const salesRows = preferredSalesRows.length
|
||||
? preferredSalesRows
|
||||
: fallbackSalesRows.length
|
||||
? fallbackSalesRows
|
||||
: [rows.find((row) => row.membershipRole === "admin") ?? first];
|
||||
const admin =
|
||||
byEmail(UAT_EMAILS.admin) ??
|
||||
rows.find((row) => row.membershipRole === "admin") ??
|
||||
rows.find((row) => row.systemRole === "super_admin") ??
|
||||
first;
|
||||
|
||||
return {
|
||||
admin,
|
||||
marketing: byRole("sales_support") ?? admin,
|
||||
sales: salesRows.length ? salesRows : [admin],
|
||||
salesManager: byRole("sales_manager") ?? admin,
|
||||
departmentManager: byRole("department_manager") ?? admin,
|
||||
topManager: byRole("top_manager") ?? admin,
|
||||
marketing:
|
||||
byEmail(UAT_EMAILS.marketing) ??
|
||||
byRole("marketing") ??
|
||||
byRole("sales_support") ??
|
||||
admin,
|
||||
sales: salesRows,
|
||||
salesByProductType: {
|
||||
crane: byEmail(UAT_EMAILS.salesCrane) ?? preferredSalesRows[0] ?? salesRows[0],
|
||||
dockdoor:
|
||||
byEmail(UAT_EMAILS.salesDockDoor) ??
|
||||
preferredSalesRows.at(-1) ??
|
||||
salesRows.at(-1) ??
|
||||
salesRows[0],
|
||||
},
|
||||
salesManager: byEmail(UAT_EMAILS.salesManager) ?? byRole("sales_manager") ?? admin,
|
||||
departmentManager:
|
||||
byEmail(UAT_EMAILS.salesManager) ??
|
||||
byRole("department_manager") ??
|
||||
byRole("sales_manager") ??
|
||||
admin,
|
||||
topManager: byEmail(UAT_EMAILS.topManager) ?? byRole("top_manager") ?? admin,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1395,7 +1455,7 @@ function buildLeadAndOpportunityData(
|
||||
}
|
||||
const contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
||||
const createdAt = daysAgo(88 - storyIndex * 4);
|
||||
const assignedUser = choose(users.sales, storyIndex);
|
||||
const assignedUser = pickSalesUser(users, story.productTypeCode, storyIndex);
|
||||
const assignedAt = addDays(createdAt, 4);
|
||||
const statusId = getOptionId(
|
||||
optionMap,
|
||||
@@ -2004,7 +2064,7 @@ function buildQuotationData(
|
||||
const customer = storyCustomerMap.get(story.slug);
|
||||
if (!opportunity || !customer) continue;
|
||||
const contact = primaryContactByCustomerId.get(customer.id) ?? null;
|
||||
const salesperson = choose(users.sales, storyIndex);
|
||||
const salesperson = pickSalesUser(users, story.productTypeCode, storyIndex);
|
||||
const baseDate = addDays(opportunity.createdAt, 10);
|
||||
const planRevisionCount =
|
||||
story.quotePlan === "returned_then_revised"
|
||||
@@ -2067,7 +2127,7 @@ function buildQuotationData(
|
||||
|
||||
quotations.push({
|
||||
id: quotationId,
|
||||
code: buildDocumentCode("QT", quotationSequence++),
|
||||
code: buildQuotationCode(story.productTypeCode, quotationSequence++),
|
||||
opportunityId: opportunity.id,
|
||||
customerId: customer.id,
|
||||
contactId: contact?.id ?? null,
|
||||
@@ -2405,7 +2465,10 @@ function buildQuotationData(
|
||||
const quotationId = crypto.randomUUID();
|
||||
quotations.push({
|
||||
id: quotationId,
|
||||
code: buildDocumentCode("QT", quotationSequence++),
|
||||
code: buildQuotationCode(
|
||||
choose(["crane", "dockdoor", "solarcell"], quotations.length),
|
||||
quotationSequence++
|
||||
),
|
||||
opportunityId: null,
|
||||
customerId: customer.id,
|
||||
contactId: contact?.id ?? null,
|
||||
|
||||
129
src/db/seeds/system-organization.seed.ts
Normal file
129
src/db/seeds/system-organization.seed.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { getDefaultPermissions } from '../../lib/auth/rbac.ts';
|
||||
import { createSqlClient, deterministicId, type SqlClient } from '../../../scripts/lib/seed-utils.ts';
|
||||
|
||||
type UserRow = {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const DEFAULT_ORGANIZATION = {
|
||||
id: deterministicId('organization:alla-demo'),
|
||||
name: 'ALLA Demo Organization',
|
||||
slug: 'alla-demo',
|
||||
membershipId: deterministicId('membership:alla-demo:super-admin')
|
||||
};
|
||||
|
||||
async function resolveSuperAdmin(sql: SqlClient): Promise<UserRow> {
|
||||
const email = process.env.SUPER_ADMIN_EMAIL?.trim().toLowerCase();
|
||||
|
||||
if (!email) {
|
||||
throw new Error('SUPER_ADMIN_EMAIL is required before running system organization seed.');
|
||||
}
|
||||
|
||||
const [user] = await sql<UserRow[]>`
|
||||
select id, email
|
||||
from users
|
||||
where email = ${email}
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Super admin user not found. Run seed-super-admin first.');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function ensureOrganization(sql: SqlClient, superAdmin: UserRow) {
|
||||
await sql`
|
||||
insert into organizations (
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
plan,
|
||||
created_by
|
||||
) values (
|
||||
${DEFAULT_ORGANIZATION.id},
|
||||
${DEFAULT_ORGANIZATION.name},
|
||||
${DEFAULT_ORGANIZATION.slug},
|
||||
${'enterprise'},
|
||||
${superAdmin.id}
|
||||
)
|
||||
on conflict (slug) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
plan = excluded.plan,
|
||||
updated_at = now()
|
||||
`;
|
||||
|
||||
const membershipPermissions = getDefaultPermissions('admin', 'crm_admin');
|
||||
const [existingMembership] = await sql<{ id: string }[]>`
|
||||
select id
|
||||
from memberships
|
||||
where user_id = ${superAdmin.id}
|
||||
and organization_id = ${DEFAULT_ORGANIZATION.id}
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (existingMembership) {
|
||||
await sql`
|
||||
update memberships
|
||||
set
|
||||
role = ${'admin'},
|
||||
business_role = ${'crm_admin'},
|
||||
permissions = ${sql.array(membershipPermissions)},
|
||||
updated_at = now()
|
||||
where id = ${existingMembership.id}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into memberships (
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
business_role,
|
||||
permissions,
|
||||
branch_scope_ids,
|
||||
product_type_scope_ids
|
||||
) values (
|
||||
${DEFAULT_ORGANIZATION.membershipId},
|
||||
${superAdmin.id},
|
||||
${DEFAULT_ORGANIZATION.id},
|
||||
${'admin'},
|
||||
${'crm_admin'},
|
||||
${sql.array(membershipPermissions)},
|
||||
${sql.array([])},
|
||||
${sql.array([])}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await sql`
|
||||
update users
|
||||
set
|
||||
active_organization_id = ${DEFAULT_ORGANIZATION.id},
|
||||
updated_at = now()
|
||||
where id = ${superAdmin.id}
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sql = createSqlClient();
|
||||
|
||||
try {
|
||||
const superAdmin = await resolveSuperAdmin(sql);
|
||||
await ensureOrganization(sql, superAdmin);
|
||||
console.log(`[seed:system-org] Ready ${DEFAULT_ORGANIZATION.name}`);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
'[seed:system-org] Failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const NODE_TS_RUNTIME_ARGS = [
|
||||
'--disable-warning=ExperimentalWarning',
|
||||
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||
'--experimental-strip-types'
|
||||
];
|
||||
|
||||
function run(label: string, args: string[]) {
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
stdio: 'inherit',
|
||||
@@ -13,7 +19,12 @@ function run(label: string, args: string[]) {
|
||||
|
||||
function main() {
|
||||
run('super admin seed', ['scripts/seed-super-admin.js']);
|
||||
run('foundation seed', ['--experimental-strip-types', 'src/db/seeds/foundation.seed.ts']);
|
||||
run('system organization seed', [
|
||||
...NODE_TS_RUNTIME_ARGS,
|
||||
'src/db/seeds/system-organization.seed.ts'
|
||||
]);
|
||||
run('foundation seed', [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/foundation.seed.ts']);
|
||||
run('pdf template seed', [...NODE_TS_RUNTIME_ARGS, 'scripts/reseed-pdf-template-version.ts']);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
431
src/db/seeds/uat-users.seed.ts
Normal file
431
src/db/seeds/uat-users.seed.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { getDefaultPermissions, getDefaultRoleProfiles } from '../../lib/auth/rbac.ts';
|
||||
import { createSqlClient, deterministicId, type SqlClient } from '../../../scripts/lib/seed-utils.ts';
|
||||
|
||||
type OrganizationRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdBy: string;
|
||||
};
|
||||
|
||||
type OptionRow = {
|
||||
id: string;
|
||||
category: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type RoleProfileRow = {
|
||||
id: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
type UatRoleAssignmentSeed = {
|
||||
roleCode: string;
|
||||
isPrimary: boolean;
|
||||
branchScopeMode: 'all' | 'selected' | 'inherit';
|
||||
branchCodes?: string[];
|
||||
productTypeScopeMode: 'all' | 'selected' | 'inherit';
|
||||
productTypeCodes?: string[];
|
||||
};
|
||||
|
||||
type UatUserSeed = {
|
||||
key: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
systemRole: 'super_admin' | 'user';
|
||||
membershipRole: 'admin' | 'user';
|
||||
businessRole:
|
||||
| 'marketing'
|
||||
| 'sales'
|
||||
| 'sales_support'
|
||||
| 'sales_manager'
|
||||
| 'department_manager'
|
||||
| 'top_manager'
|
||||
| 'crm_admin';
|
||||
roleAssignments: UatRoleAssignmentSeed[];
|
||||
};
|
||||
|
||||
const UAT_PASSWORD = 'UatDemo123!';
|
||||
|
||||
const UAT_USERS: UatUserSeed[] = [
|
||||
{
|
||||
key: 'mk',
|
||||
name: 'MK UAT',
|
||||
email: 'mk.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'marketing',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'marketing',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'sales-crane',
|
||||
name: 'Sales Crane UAT',
|
||||
email: 'sales.crane.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'selected',
|
||||
productTypeCodes: ['crane']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'sales-dockdoor',
|
||||
name: 'Sales Dock Door UAT',
|
||||
email: 'sales.dockdoor.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'selected',
|
||||
productTypeCodes: ['dockdoor']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'mgr-sales',
|
||||
name: 'Sales Manager UAT',
|
||||
email: 'mgr.sales.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'sales_manager',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'sales_manager',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
},
|
||||
{
|
||||
roleCode: 'department_manager',
|
||||
isPrimary: false,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'ceo',
|
||||
name: 'CEO UAT',
|
||||
email: 'ceo.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'user',
|
||||
businessRole: 'top_manager',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'top_manager',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
name: 'Admin UAT',
|
||||
email: 'admin.uat@alla.local',
|
||||
password: UAT_PASSWORD,
|
||||
systemRole: 'user',
|
||||
membershipRole: 'admin',
|
||||
businessRole: 'crm_admin',
|
||||
roleAssignments: [
|
||||
{
|
||||
roleCode: 'crm_admin',
|
||||
isPrimary: true,
|
||||
branchScopeMode: 'all',
|
||||
productTypeScopeMode: 'all'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
async function ensureRoleProfiles(
|
||||
sql: SqlClient,
|
||||
organization: OrganizationRow
|
||||
): Promise<Map<string, RoleProfileRow>> {
|
||||
const defaultProfiles = getDefaultRoleProfiles();
|
||||
|
||||
for (const profile of defaultProfiles) {
|
||||
await sql`
|
||||
insert into crm_role_profiles (
|
||||
id,
|
||||
organization_id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
branch_scope_mode,
|
||||
product_scope_mode,
|
||||
ownership_scope,
|
||||
approval_authority,
|
||||
is_system,
|
||||
is_active,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${deterministicId(`crm-role-profile:${organization.id}:${profile.code}`)},
|
||||
${organization.id},
|
||||
${profile.code},
|
||||
${profile.name},
|
||||
${profile.description},
|
||||
${sql.array(profile.permissions)},
|
||||
${profile.branchScopeMode},
|
||||
${profile.productScopeMode},
|
||||
${profile.ownershipScope},
|
||||
${profile.approvalAuthority},
|
||||
${profile.isSystem},
|
||||
${true},
|
||||
${organization.createdBy},
|
||||
${organization.createdBy}
|
||||
)
|
||||
on conflict (organization_id, code) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
permissions = excluded.permissions,
|
||||
branch_scope_mode = excluded.branch_scope_mode,
|
||||
product_scope_mode = excluded.product_scope_mode,
|
||||
ownership_scope = excluded.ownership_scope,
|
||||
approval_authority = excluded.approval_authority,
|
||||
is_system = excluded.is_system,
|
||||
is_active = excluded.is_active,
|
||||
updated_by = excluded.updated_by,
|
||||
deleted_at = null,
|
||||
updated_at = now()
|
||||
`;
|
||||
}
|
||||
|
||||
const rows = await sql<RoleProfileRow[]>`
|
||||
select id, code
|
||||
from crm_role_profiles
|
||||
where organization_id = ${organization.id}
|
||||
and deleted_at is null
|
||||
`;
|
||||
|
||||
return new Map(rows.map((row) => [row.code, row]));
|
||||
}
|
||||
|
||||
async function loadOptions(sql: SqlClient, organizationId: string): Promise<Map<string, OptionRow>> {
|
||||
const rows = await sql<OptionRow[]>`
|
||||
select id, category, code
|
||||
from ms_options
|
||||
where organization_id = ${organizationId}
|
||||
and deleted_at is null
|
||||
and is_active = true
|
||||
`;
|
||||
|
||||
return new Map(rows.map((row) => [`${row.category}:${row.code}`, row]));
|
||||
}
|
||||
|
||||
function resolveScopeIds(
|
||||
optionMap: Map<string, OptionRow>,
|
||||
category: string,
|
||||
codes: string[] | undefined
|
||||
): string[] {
|
||||
if (!codes?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return codes.map((code) => {
|
||||
const row = optionMap.get(`${category}:${code}`);
|
||||
|
||||
if (!row) {
|
||||
throw new Error(`Missing ${category} option for code ${code}`);
|
||||
}
|
||||
|
||||
return row.id;
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertUatUser(
|
||||
sql: SqlClient,
|
||||
organization: OrganizationRow,
|
||||
optionMap: Map<string, OptionRow>,
|
||||
roleProfileMap: Map<string, RoleProfileRow>,
|
||||
seed: UatUserSeed
|
||||
) {
|
||||
const passwordHash = await bcrypt.hash(seed.password, 10);
|
||||
const [userRow] = await sql<{ id: string }[]>`
|
||||
insert into users (
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
system_role,
|
||||
active_organization_id
|
||||
) values (
|
||||
${deterministicId(`uat-user:${seed.email}`)},
|
||||
${seed.name},
|
||||
${seed.email},
|
||||
${passwordHash},
|
||||
${seed.systemRole},
|
||||
${organization.id}
|
||||
)
|
||||
on conflict (email) do update
|
||||
set
|
||||
name = excluded.name,
|
||||
password_hash = excluded.password_hash,
|
||||
system_role = excluded.system_role,
|
||||
active_organization_id = excluded.active_organization_id,
|
||||
updated_at = now()
|
||||
returning id
|
||||
`;
|
||||
|
||||
const membershipPermissions = getDefaultPermissions(seed.membershipRole, seed.businessRole);
|
||||
const existingMembership =
|
||||
(
|
||||
await sql<{ id: string }[]>`
|
||||
select id
|
||||
from memberships
|
||||
where user_id = ${userRow.id}
|
||||
and organization_id = ${organization.id}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
|
||||
if (existingMembership) {
|
||||
await sql`
|
||||
update memberships
|
||||
set
|
||||
role = ${seed.membershipRole},
|
||||
business_role = ${seed.businessRole},
|
||||
permissions = ${sql.array(membershipPermissions)},
|
||||
branch_scope_ids = ${sql.array([])},
|
||||
product_type_scope_ids = ${sql.array([])},
|
||||
updated_at = now()
|
||||
where id = ${existingMembership.id}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into memberships (
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
business_role,
|
||||
permissions,
|
||||
branch_scope_ids,
|
||||
product_type_scope_ids
|
||||
) values (
|
||||
${deterministicId(`membership:${organization.id}:${seed.email}`)},
|
||||
${userRow.id},
|
||||
${organization.id},
|
||||
${seed.membershipRole},
|
||||
${seed.businessRole},
|
||||
${sql.array(membershipPermissions)},
|
||||
${sql.array([])},
|
||||
${sql.array([])}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await sql`
|
||||
delete from crm_user_role_assignments
|
||||
where organization_id = ${organization.id}
|
||||
and user_id = ${userRow.id}
|
||||
`;
|
||||
|
||||
for (const assignment of seed.roleAssignments) {
|
||||
const roleProfile = roleProfileMap.get(assignment.roleCode);
|
||||
|
||||
if (!roleProfile) {
|
||||
throw new Error(`Missing CRM role profile ${assignment.roleCode}`);
|
||||
}
|
||||
|
||||
await sql`
|
||||
insert into crm_user_role_assignments (
|
||||
id,
|
||||
organization_id,
|
||||
user_id,
|
||||
role_profile_id,
|
||||
branch_scope_mode,
|
||||
branch_scope_ids,
|
||||
product_type_scope_mode,
|
||||
product_type_scope_ids,
|
||||
is_primary,
|
||||
is_active,
|
||||
assigned_by,
|
||||
assigned_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
) values (
|
||||
${deterministicId(`crm-role-assignment:${organization.id}:${seed.email}:${assignment.roleCode}`)},
|
||||
${organization.id},
|
||||
${userRow.id},
|
||||
${roleProfile.id},
|
||||
${assignment.branchScopeMode},
|
||||
${sql.array(resolveScopeIds(optionMap, 'crm_branch', assignment.branchCodes))},
|
||||
${assignment.productTypeScopeMode},
|
||||
${sql.array(resolveScopeIds(optionMap, 'crm_product_type', assignment.productTypeCodes))},
|
||||
${assignment.isPrimary},
|
||||
${true},
|
||||
${organization.createdBy},
|
||||
${new Date()},
|
||||
${new Date()},
|
||||
${new Date()},
|
||||
${null}
|
||||
)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function seedOrganization(sql: SqlClient, organization: OrganizationRow) {
|
||||
const optionMap = await loadOptions(sql, organization.id);
|
||||
const roleProfileMap = await ensureRoleProfiles(sql, organization);
|
||||
|
||||
for (const seed of UAT_USERS) {
|
||||
await upsertUatUser(sql, organization, optionMap, roleProfileMap, seed);
|
||||
}
|
||||
|
||||
console.log(`[seed:uat-users] ${organization.name}: ${UAT_USERS.length} UAT users ready`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sql = createSqlClient();
|
||||
|
||||
try {
|
||||
const organizations = await sql<OrganizationRow[]>`
|
||||
select id, name, created_by as "createdBy"
|
||||
from organizations
|
||||
order by name asc
|
||||
`;
|
||||
|
||||
if (organizations.length === 0) {
|
||||
throw new Error('No organizations found. Run seed:system first.');
|
||||
}
|
||||
|
||||
for (const organization of organizations) {
|
||||
await seedOrganization(sql, organization);
|
||||
}
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[seed:uat-users] Failed:', error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,17 +1,32 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const NODE_TS_RUNTIME_ARGS = [
|
||||
'--disable-warning=ExperimentalWarning',
|
||||
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||
'--experimental-strip-types'
|
||||
];
|
||||
|
||||
function main() {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-strip-types', 'src/db/seeds/crm-uat.seed.ts'],
|
||||
const steps: Array<{ label: string; args: string[] }> = [
|
||||
{
|
||||
label: 'uat users seed',
|
||||
args: [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/uat-users.seed.ts']
|
||||
},
|
||||
{
|
||||
label: 'crm uat data seed',
|
||||
args: [...NODE_TS_RUNTIME_ARGS, 'src/db/seeds/crm-uat.seed.ts']
|
||||
}
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
const result = spawnSync(process.execPath, step.args, {
|
||||
stdio: 'inherit',
|
||||
env: process.env
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[seed:uat] failed with exit code ${result.status ?? 1}`);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`[seed:uat] ${step.label} failed with exit code ${result.status ?? 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const signInSchema = z.object({
|
||||
|
||||
export default function SignInViewPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard/crm";
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const form = useAppForm({
|
||||
|
||||
@@ -610,7 +610,7 @@ async function canAccessCustomerRow(
|
||||
accessContext,
|
||||
));
|
||||
|
||||
if (accessContext.ownershipScope === "monitor") {
|
||||
if (accessContext.ownershipScope === 'monitor') {
|
||||
return opportunityCustomerIds.has(row.id);
|
||||
}
|
||||
|
||||
|
||||
44
src/proxy.ts
44
src/proxy.ts
@@ -1,29 +1,39 @@
|
||||
import { auth } from '@/auth';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from "@/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const protectedApiPrefixes = ['/api/products', '/api/organizations', '/api/users'];
|
||||
const superAdminOnlyRoutes = ['/dashboard/workspaces'];
|
||||
const organizationAdminRoutes = ['/dashboard/users', '/dashboard/workspaces/team'];
|
||||
const protectedApiPrefixes = [
|
||||
"/api/products",
|
||||
"/api/organizations",
|
||||
"/api/users",
|
||||
];
|
||||
const superAdminOnlyRoutes = ["/dashboard/workspaces"];
|
||||
const organizationAdminRoutes = [
|
||||
"/dashboard/users",
|
||||
"/dashboard/workspaces/team",
|
||||
];
|
||||
|
||||
export default auth((req) => {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const isDashboardRoute = pathname.startsWith('/dashboard');
|
||||
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) => pathname.startsWith(prefix));
|
||||
const isDashboardRoute = pathname.startsWith("/dashboard/crm");
|
||||
const isProtectedApiRoute = protectedApiPrefixes.some((prefix) =>
|
||||
pathname.startsWith(prefix),
|
||||
);
|
||||
const user = req.auth?.user;
|
||||
const isSignedIn = !!user;
|
||||
const isSuperAdmin = user?.systemRole === 'super_admin';
|
||||
const isSuperAdmin = user?.systemRole === "super_admin";
|
||||
const isOrganizationAdmin =
|
||||
isSuperAdmin ||
|
||||
(user?.activeOrganizationId &&
|
||||
(user.activeMembershipRole === 'admin' || user.activePermissions?.includes('users:manage')));
|
||||
(user.activeMembershipRole === "admin" ||
|
||||
user.activePermissions?.includes("users:manage")));
|
||||
|
||||
if (!isSignedIn && (isDashboardRoute || isProtectedApiRoute)) {
|
||||
if (isProtectedApiRoute) {
|
||||
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const signInUrl = new URL('/auth/sign-in', req.nextUrl.origin);
|
||||
signInUrl.searchParams.set('callbackUrl', pathname);
|
||||
const signInUrl = new URL("/auth/sign-in", req.nextUrl.origin);
|
||||
signInUrl.searchParams.set("callbackUrl", pathname);
|
||||
return NextResponse.redirect(signInUrl);
|
||||
}
|
||||
|
||||
@@ -32,7 +42,7 @@ export default auth((req) => {
|
||||
superAdminOnlyRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||
!isSuperAdmin
|
||||
) {
|
||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
||||
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -40,7 +50,7 @@ export default auth((req) => {
|
||||
organizationAdminRoutes.some((prefix) => pathname.startsWith(prefix)) &&
|
||||
!isOrganizationAdmin
|
||||
) {
|
||||
return NextResponse.redirect(new URL('/dashboard', req.nextUrl.origin));
|
||||
return NextResponse.redirect(new URL("/dashboard", req.nextUrl.origin));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
@@ -48,7 +58,7 @@ export default auth((req) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||
'/(api|trpc)(.*)'
|
||||
]
|
||||
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||
"/(api|trpc)(.*)",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user