Skip to content

Commit 542e2b7

Browse files
authored
Merge pull request #512 from trycompai/main
[comp] Production Deploy
2 parents 5ea1a6d + dacb4fe commit 542e2b7

20 files changed

Lines changed: 4403 additions & 265 deletions

.github/workflows/release.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- release
7+
8+
permissions:
9+
contents: write # Allow check out and commit changes (version, changelog)
10+
issues: write # Allow commenting on issues/PRs
11+
pull-requests: write # Allow commenting on issues/PRs
12+
id-token: write # Needed for provenance
13+
14+
jobs:
15+
release:
16+
name: Release
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
# Fetch all history so semantic-release can analyze commits
23+
fetch-depth: 0
24+
# Use a token that has permission to push to the repository
25+
# Either a PAT stored in GH_TOKEN or the default GITHUB_TOKEN
26+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: '20' # Or your preferred Node.js version
32+
33+
- name: Setup Bun
34+
uses: oven-sh/setup-bun@v1
35+
# with:
36+
# bun-version: latest # Optional: specify a bun version
37+
38+
- name: Install dependencies
39+
run: bun install --frozen-lockfile # Use --frozen-lockfile in CI
40+
41+
- name: Release
42+
env:
43+
GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
44+
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Uncomment if publishing to npm
45+
run: npx semantic-release

.husky/commit-msg

100755100644
Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
3-
4-
export NVM_DIR="$HOME/.nvm"
5-
. "$NVM_DIR/nvm.sh"
6-
7-
export PATH=$PATH:$HOME/.nvm/versions/node/$(nvm current)/bin
8-
9-
npx --version
10-
11-
npx commitlint --edit $1
1+
npx commitlint --edit $1

.husky/pre-commit

Lines changed: 0 additions & 13 deletions
This file was deleted.

.husky/pre-push

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
3-
41
branch_name=$(git symbolic-ref --short HEAD)
5-
pattern="^(claudio|mariano|alex)/leap-.*$"
2+
pattern="^[a-zA-Z]+/comp-.*$"
63

74
if [[ ! $branch_name =~ $pattern ]]; then
85
echo "Branch name '$branch_name' does not follow the naming convention."
6+
echo "Branch names should follow the pattern: firstname/comp-*"
97
exit 1
108
fi
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Alert, AlertDescription, AlertTitle } from "@comp/ui/alert";
2+
import { AlertTriangle } from "lucide-react";
3+
4+
interface NoAccessMessageProps {
5+
message?: string;
6+
}
7+
8+
export function NoAccessMessage({ message }: NoAccessMessageProps) {
9+
return (
10+
<Alert variant="destructive" className="max-w-md mx-auto">
11+
<AlertTriangle className="h-4 w-4" />
12+
<AlertTitle>Access Denied</AlertTitle>
13+
<AlertDescription>
14+
{message ?? "You do not have access to the employee portal with this account, or you are not currently assigned to an organization. Please contact your administrator if you believe this is an error."}
15+
</AlertDescription>
16+
</Alert>
17+
);
18+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { db } from "@comp/db";
2+
import type {
3+
Member,
4+
User,
5+
Policy,
6+
EmployeeTrainingVideoCompletion,
7+
Organization
8+
} from "@prisma/client";
9+
import { EmployeeTasksList } from "./EmployeeTasksList";
10+
11+
// Define the type for the member prop passed from Overview
12+
interface MemberWithUserOrg extends Member {
13+
user: User;
14+
organization: Organization;
15+
}
16+
17+
interface OrganizationDashboardProps {
18+
organizationId: string;
19+
member: MemberWithUserOrg; // Pass the full member object for user info etc.
20+
}
21+
22+
export async function OrganizationDashboard({ organizationId, member }: OrganizationDashboardProps) {
23+
24+
// Fetch policies specific to the selected organization
25+
const policies = await db.policy.findMany({
26+
where: {
27+
organizationId: organizationId,
28+
isRequiredToSign: true, // Keep original logic for required policies
29+
},
30+
});
31+
32+
// Fetch training video completions specific to the member
33+
// Note: The original fetched *all* completions for the member, regardless of org
34+
// If videos are org-specific, the schema/query might need adjustment
35+
const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({
36+
where: {
37+
memberId: member.id,
38+
// Add organizationId filter if EmployeeTrainingVideoCompletion has it
39+
// organizationId: organizationId,
40+
},
41+
// Include video details if needed by EmployeeTasksList
42+
// include: { trainingVideo: true }
43+
});
44+
45+
// Display welcome message and tasks
46+
return (
47+
<div className="space-y-6">
48+
<div className="flex flex-col gap-1">
49+
{/* Use organization name if available and needed */}
50+
<p className="text-sm text-muted-foreground">Organization: {member.organization.name}</p>
51+
<h1 className="text-2xl font-bold">Welcome back, {member.user.name}</h1>
52+
<p className="text-sm">Please complete the following tasks for {member.organization.name}:</p>
53+
</div>
54+
<EmployeeTasksList
55+
policies={policies}
56+
trainingVideos={trainingVideos}
57+
member={member} // Pass the member object down
58+
/>
59+
</div>
60+
);
61+
}
Lines changed: 102 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,136 @@
11
import { auth } from "@/app/lib/auth";
22
import { db } from "@comp/db";
3-
import { cache } from "react";
3+
// Import types directly from @prisma/client
4+
import type {
5+
Member,
6+
User,
7+
Policy,
8+
EmployeeTrainingVideoCompletion,
9+
Organization,
10+
} from "@prisma/client";
411
import { headers } from "next/headers";
5-
import { EmployeeTasksList } from "./EmployeeTasksList";
6-
7-
export async function Overview() {
8-
const policies = await getPolicies();
9-
const trainingVideos = await getTrainingVideos();
10-
const member = await getMember();
11-
12-
return (
13-
<div className="space-y-6">
14-
<div className="flex flex-col gap-1">
15-
<h1 className="text-2xl font-bold">Welcome back, {member.user.name}</h1>
16-
<p className="text-sm">Please complete the following tasks</p>
17-
</div>
18-
<EmployeeTasksList
19-
policies={policies}
20-
trainingVideos={trainingVideos}
21-
member={member}
22-
/>
23-
</div>
24-
);
12+
import { redirect } from "next/navigation";
13+
// Removed EmployeeTasksList import as it's not used directly here
14+
import { NoAccessMessage } from "./NoAccessMessage";
15+
// Removed OrganizationSelector import
16+
import { OrganizationDashboard } from "./OrganizationDashboard";
17+
18+
// Define the type for the member prop including the user and organization relations
19+
interface MemberWithUserOrg extends Member {
20+
user: User;
21+
organization: Organization;
2522
}
2623

27-
const getMember = cache(async () => {
24+
// Removed OverviewProps interface and searchParams prop
25+
// export async function Overview({ searchParams }: OverviewProps) {
26+
export async function Overview() {
2827
const session = await auth.api.getSession({
2928
headers: await headers(),
3029
});
3130

3231
if (!session?.user) {
33-
throw new Error("Unauthorized");
32+
redirect("/login"); // Or appropriate login/auth route
3433
}
3534

36-
const member = await db.member.findFirst({
35+
// Fetch all memberships for the user, including organization details
36+
const memberships = await db.member.findMany({
3737
where: {
3838
userId: session.user.id,
39-
role: "employee",
39+
// We might want to filter by role if needed, but let's see all memberships first
40+
// role: "employee", // Keep commented unless needed
4041
},
4142
include: {
4243
user: true,
44+
organization: true, // Include organization details
4345
},
4446
});
4547

46-
if (!member) {
47-
throw new Error("Unauthorized");
48+
// Case 1: No memberships found
49+
if (memberships.length === 0) {
50+
return <NoAccessMessage />;
4851
}
4952

50-
return member;
51-
});
52-
53-
const getPolicies = cache(async () => {
54-
const member = await getMember();
55-
const organizationId = member.organizationId;
53+
// Filter memberships to only those with valid organization data
54+
const validMemberships = memberships.filter(
55+
(member): member is MemberWithUserOrg & { organization: Organization } =>
56+
Boolean(member.organization)
57+
);
5658

57-
if (!organizationId) {
58-
throw new Error("Unauthorized");
59+
// If after filtering, there are no valid memberships with organizations
60+
if (validMemberships.length === 0) {
61+
// This case might indicate memberships exist but lack organization links
62+
console.warn("User has memberships but none with associated organizations.", { userId: session.user.id });
63+
return <NoAccessMessage message="You don't seem to belong to any organizations currently." />;
5964
}
6065

61-
const policies = await db.policy.findMany({
62-
where: {
63-
organizationId,
64-
isRequiredToSign: true,
65-
},
66-
});
6766

68-
return policies;
69-
});
67+
// Render a dashboard for each valid membership
68+
return (
69+
<div className="space-y-8"> {/* Added a wrapper div with spacing */}
70+
{validMemberships.map((member) => (
71+
<OrganizationDashboard
72+
key={member.organizationId} // Use organizationId as key
73+
organizationId={member.organizationId}
74+
member={member} // Pass the full member object (already includes org)
75+
/>
76+
))}
77+
</div>
78+
);
79+
80+
// Removed the logic for OrganizationSelector and single/selected org handling
81+
/*
82+
// Extract unique organizations
83+
const organizations = memberships.reduce((acc, member) => {
84+
if (member.organization && !acc.some(org => org.id === member.organizationId)) {
85+
acc.push(member.organization);
86+
}
87+
return acc;
88+
}, [] as Organization[]);
89+
90+
91+
const selectedOrgId = searchParams?.orgId as string | undefined;
7092
71-
const getTrainingVideos = cache(async () => {
72-
const member = await getMember();
93+
// Case 2: Multiple organizations, and none selected yet OR selected is invalid
94+
if (organizations.length > 1) {
95+
const isValidSelection = selectedOrgId && organizations.some(org => org.id === selectedOrgId);
7396
74-
if (!member) {
75-
throw new Error("Unauthorized");
97+
if (!isValidSelection) {
98+
// If multiple orgs and no valid selection, show selector
99+
return <OrganizationSelector organizations={organizations} />;
100+
}
101+
// If valid selection, proceed to find member and render dashboard (handled below)
76102
}
77103
78-
const organizationId = member.organizationId;
104+
// Case 3: Exactly one organization OR multiple orgs with a valid selection
105+
let targetOrgId: string | undefined = undefined;
106+
let targetMember: MemberWithUserOrg | undefined = undefined;
107+
108+
if (organizations.length === 1) {
109+
targetOrgId = organizations[0].id;
110+
// Find the specific membership for this single organization
111+
targetMember = memberships.find(m => m.organizationId === targetOrgId);
112+
} else if (selectedOrgId) {
113+
// Already validated that selectedOrgId is one of the user's orgs
114+
targetOrgId = selectedOrgId;
115+
targetMember = memberships.find(m => m.organizationId === targetOrgId);
116+
}
79117
80-
if (!organizationId) {
81-
throw new Error("Unauthorized");
118+
// If we have a target organization and member, render the dashboard
119+
if (targetOrgId && targetMember) {
120+
// We need the full MemberWithUserOrg type here potentially
121+
// Ensure targetMember is correctly typed if OrganizationDashboard expects more
122+
return <OrganizationDashboard organizationId={targetOrgId} member={targetMember as MemberWithUserOrg} />;
82123
}
83124
84-
const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({
85-
where: {
86-
memberId: member.id,
87-
},
88-
});
125+
// Fallback case (should ideally not be reached with the logic above)
126+
// If multiple orgs but somehow didn't render selector or dashboard
127+
if (organizations.length > 1) {
128+
return <OrganizationSelector organizations={organizations} />;
129+
}
89130
90-
return trainingVideos;
91-
});
131+
// If single org but couldn't find member (data inconsistency?)
132+
// Or some other unexpected state
133+
console.error("Unexpected state in Overview component", { userId: session.user.id, memberships });
134+
return <NoAccessMessage message="An unexpected error occurred. Please contact support." />; // Or a more specific error
135+
*/
136+
}

apps/portal/src/app/[locale]/(app)/(home)/layout.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ export default async function Layout({
99
const t = await getI18n();
1010

1111
return (
12-
<div className="max-w-[1200px]">
12+
<>
1313
<SecondaryMenu items={[{ path: "/", label: t("sidebar.dashboard") }]} />
1414

15-
<main className="mt-8">{children}</main>
16-
</div>
15+
<div className="mt-8">
16+
{children}
17+
</div>
18+
</>
1719
);
1820
}

0 commit comments

Comments
 (0)