Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Release

on:
push:
branches:
- release

permissions:
contents: write # Allow check out and commit changes (version, changelog)
issues: write # Allow commenting on issues/PRs
pull-requests: write # Allow commenting on issues/PRs
id-token: write # Needed for provenance

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Fetch all history so semantic-release can analyze commits
fetch-depth: 0
# Use a token that has permission to push to the repository
# Either a PAT stored in GH_TOKEN or the default GITHUB_TOKEN
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Or your preferred Node.js version

- name: Setup Bun
uses: oven-sh/setup-bun@v1
# with:
# bun-version: latest # Optional: specify a bun version

- name: Install dependencies
run: bun install --frozen-lockfile # Use --frozen-lockfile in CI

- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Uncomment if publishing to npm
run: npx semantic-release
12 changes: 1 addition & 11 deletions .husky/commit-msg
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"

export PATH=$PATH:$HOME/.nvm/versions/node/$(nvm current)/bin

npx --version

npx commitlint --edit $1
npx commitlint --edit $1
13 changes: 0 additions & 13 deletions .husky/pre-commit

This file was deleted.

6 changes: 2 additions & 4 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

branch_name=$(git symbolic-ref --short HEAD)
pattern="^(claudio|mariano|alex)/leap-.*$"
pattern="^[a-zA-Z]+/comp-.*$"

if [[ ! $branch_name =~ $pattern ]]; then
echo "Branch name '$branch_name' does not follow the naming convention."
echo "Branch names should follow the pattern: firstname/comp-*"
exit 1
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Alert, AlertDescription, AlertTitle } from "@comp/ui/alert";
import { AlertTriangle } from "lucide-react";

interface NoAccessMessageProps {
message?: string;
}

export function NoAccessMessage({ message }: NoAccessMessageProps) {
return (
<Alert variant="destructive" className="max-w-md mx-auto">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Access Denied</AlertTitle>
<AlertDescription>
{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."}
</AlertDescription>
</Alert>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { db } from "@comp/db";
import type {
Member,
User,
Policy,
EmployeeTrainingVideoCompletion,
Organization
} from "@prisma/client";
import { EmployeeTasksList } from "./EmployeeTasksList";

// Define the type for the member prop passed from Overview
interface MemberWithUserOrg extends Member {
user: User;
organization: Organization;
}

interface OrganizationDashboardProps {
organizationId: string;
member: MemberWithUserOrg; // Pass the full member object for user info etc.
}

export async function OrganizationDashboard({ organizationId, member }: OrganizationDashboardProps) {

// Fetch policies specific to the selected organization
const policies = await db.policy.findMany({
where: {
organizationId: organizationId,
isRequiredToSign: true, // Keep original logic for required policies
},
});

// Fetch training video completions specific to the member
// Note: The original fetched *all* completions for the member, regardless of org
// If videos are org-specific, the schema/query might need adjustment
const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({
where: {
memberId: member.id,
// Add organizationId filter if EmployeeTrainingVideoCompletion has it
// organizationId: organizationId,
},
// Include video details if needed by EmployeeTasksList
// include: { trainingVideo: true }
});

// Display welcome message and tasks
return (
<div className="space-y-6">
<div className="flex flex-col gap-1">
{/* Use organization name if available and needed */}
<p className="text-sm text-muted-foreground">Organization: {member.organization.name}</p>
<h1 className="text-2xl font-bold">Welcome back, {member.user.name}</h1>
<p className="text-sm">Please complete the following tasks for {member.organization.name}:</p>
</div>
<EmployeeTasksList
policies={policies}
trainingVideos={trainingVideos}
member={member} // Pass the member object down
/>
</div>
);
}
159 changes: 102 additions & 57 deletions apps/portal/src/app/[locale]/(app)/(home)/components/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,136 @@
import { auth } from "@/app/lib/auth";
import { db } from "@comp/db";
import { cache } from "react";
// Import types directly from @prisma/client
import type {
Member,
User,
Policy,
EmployeeTrainingVideoCompletion,
Organization,
} from "@prisma/client";
import { headers } from "next/headers";
import { EmployeeTasksList } from "./EmployeeTasksList";

export async function Overview() {
const policies = await getPolicies();
const trainingVideos = await getTrainingVideos();
const member = await getMember();

return (
<div className="space-y-6">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">Welcome back, {member.user.name}</h1>
<p className="text-sm">Please complete the following tasks</p>
</div>
<EmployeeTasksList
policies={policies}
trainingVideos={trainingVideos}
member={member}
/>
</div>
);
import { redirect } from "next/navigation";
// Removed EmployeeTasksList import as it's not used directly here
import { NoAccessMessage } from "./NoAccessMessage";
// Removed OrganizationSelector import
import { OrganizationDashboard } from "./OrganizationDashboard";

// Define the type for the member prop including the user and organization relations
interface MemberWithUserOrg extends Member {
user: User;
organization: Organization;
}

const getMember = cache(async () => {
// Removed OverviewProps interface and searchParams prop
// export async function Overview({ searchParams }: OverviewProps) {
export async function Overview() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session?.user) {
throw new Error("Unauthorized");
redirect("/login"); // Or appropriate login/auth route
}

const member = await db.member.findFirst({
// Fetch all memberships for the user, including organization details
const memberships = await db.member.findMany({
where: {
userId: session.user.id,
role: "employee",
// We might want to filter by role if needed, but let's see all memberships first
// role: "employee", // Keep commented unless needed
},
include: {
user: true,
organization: true, // Include organization details
},
});

if (!member) {
throw new Error("Unauthorized");
// Case 1: No memberships found
if (memberships.length === 0) {
return <NoAccessMessage />;
}

return member;
});

const getPolicies = cache(async () => {
const member = await getMember();
const organizationId = member.organizationId;
// Filter memberships to only those with valid organization data
const validMemberships = memberships.filter(
(member): member is MemberWithUserOrg & { organization: Organization } =>
Boolean(member.organization)
);

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

const policies = await db.policy.findMany({
where: {
organizationId,
isRequiredToSign: true,
},
});

return policies;
});
// Render a dashboard for each valid membership
return (
<div className="space-y-8"> {/* Added a wrapper div with spacing */}
{validMemberships.map((member) => (
<OrganizationDashboard
key={member.organizationId} // Use organizationId as key
organizationId={member.organizationId}
member={member} // Pass the full member object (already includes org)
/>
))}
</div>
);

// Removed the logic for OrganizationSelector and single/selected org handling
/*
// Extract unique organizations
const organizations = memberships.reduce((acc, member) => {
if (member.organization && !acc.some(org => org.id === member.organizationId)) {
acc.push(member.organization);
}
return acc;
}, [] as Organization[]);


const selectedOrgId = searchParams?.orgId as string | undefined;

const getTrainingVideos = cache(async () => {
const member = await getMember();
// Case 2: Multiple organizations, and none selected yet OR selected is invalid
if (organizations.length > 1) {
const isValidSelection = selectedOrgId && organizations.some(org => org.id === selectedOrgId);

if (!member) {
throw new Error("Unauthorized");
if (!isValidSelection) {
// If multiple orgs and no valid selection, show selector
return <OrganizationSelector organizations={organizations} />;
}
// If valid selection, proceed to find member and render dashboard (handled below)
}

const organizationId = member.organizationId;
// Case 3: Exactly one organization OR multiple orgs with a valid selection
let targetOrgId: string | undefined = undefined;
let targetMember: MemberWithUserOrg | undefined = undefined;

if (organizations.length === 1) {
targetOrgId = organizations[0].id;
// Find the specific membership for this single organization
targetMember = memberships.find(m => m.organizationId === targetOrgId);
} else if (selectedOrgId) {
// Already validated that selectedOrgId is one of the user's orgs
targetOrgId = selectedOrgId;
targetMember = memberships.find(m => m.organizationId === targetOrgId);
}

if (!organizationId) {
throw new Error("Unauthorized");
// If we have a target organization and member, render the dashboard
if (targetOrgId && targetMember) {
// We need the full MemberWithUserOrg type here potentially
// Ensure targetMember is correctly typed if OrganizationDashboard expects more
return <OrganizationDashboard organizationId={targetOrgId} member={targetMember as MemberWithUserOrg} />;
}

const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({
where: {
memberId: member.id,
},
});
// Fallback case (should ideally not be reached with the logic above)
// If multiple orgs but somehow didn't render selector or dashboard
if (organizations.length > 1) {
return <OrganizationSelector organizations={organizations} />;
}

return trainingVideos;
});
// If single org but couldn't find member (data inconsistency?)
// Or some other unexpected state
console.error("Unexpected state in Overview component", { userId: session.user.id, memberships });
return <NoAccessMessage message="An unexpected error occurred. Please contact support." />; // Or a more specific error
*/
}
8 changes: 5 additions & 3 deletions apps/portal/src/app/[locale]/(app)/(home)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ export default async function Layout({
const t = await getI18n();

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

<main className="mt-8">{children}</main>
</div>
<div className="mt-8">
{children}
</div>
</>
);
}
Loading