Skip to content

Commit

Permalink
Merge pull request #7 from chapter-gtm/4-assign-an-owner-to-an-opport…
Browse files Browse the repository at this point in the history
…unity

4 assign an owner to an opportunity
  • Loading branch information
shrir authored Dec 11, 2024
2 parents f10769b + ecbe217 commit 779539f
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 162 deletions.
40 changes: 26 additions & 14 deletions frontend/src/components/opportunities/OpportunitiesMain.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client"

import { type User } from "@/types/user"
import { type Icp } from "@/types/icp"
import { type Opportunity } from "@/types/opportunity"
import { getUsers } from "@/utils/chapter/users"
import { getIcps } from "@/utils/chapter/icp"
import { getOpportunities } from "@/utils/chapter/opportunity"
import {
Expand Down Expand Up @@ -35,11 +37,12 @@ import { useEffect, useState, useCallback, useRef } from "react"
import Link from "next/link"

import { OpportunityStageList } from "./OpportunityStageList"

import { OpportunityOwner } from "./OpportunityOwner"
export function OpportunitiesMain() {
const [isPopulated, setIsPopulated] = useState(false)
const [sheetOpen, setSheetOpen] = useState(false)
const [icp, setIcp] = useState<Icp | null>(null)
const [allUsers, setAllUsers] = useState<User[]>([])
const icpRef = useRef(icp)
const [records, setRecords] = useState<RecordSchema[]>([])
const [recordColumns, setRecordColumns] = useState<ColumnDef<RecordSchema>[]>(
Expand All @@ -51,13 +54,14 @@ export function OpportunitiesMain() {
const opportunityMapRef = useRef(opportunityMap)

const [selectedRow, setSelectedRow] = useState<Opportunity | null>(null)
const [selectedRows, setSelectedRows] = useState<Opportunity[]>([])
const [preSelectedFilters, setPreSelectedFilters] =
useState<ColumnFiltersState>([])

useEffect(() => {
const fetchIcpAndOpportunities = async () => {
try {
const users = await getUsers()

const currentUserIcps = await getIcps()
if (currentUserIcps === null || currentUserIcps.length <= 0) {
throw new Error("Failed to fetch ICP")
Expand Down Expand Up @@ -91,10 +95,12 @@ export function OpportunitiesMain() {
tools: rec.jobPosts?.flatMap((jobPost) => jobPost.tools),
processes: rec.jobPosts?.flatMap((jobPost) => jobPost.processes),
investors: rec.company?.lastFunding?.investors,
owner: rec.owner,
}
return record
})
)
setAllUsers(users)
setIcp(currentUserIcps[0])
setOpportunityMap(oppMap)
setRecords(tableRecords)
Expand All @@ -104,6 +110,7 @@ export function OpportunitiesMain() {
setRecordColumns(
getRecordColumns(
currentUserIcps[0],
allUsers,
updateOpportunityCallback,
handleOpenDrawerCallback
)
Expand Down Expand Up @@ -144,7 +151,6 @@ export function OpportunitiesMain() {
}
opportunities.push(opportunity)
}
setSelectedRows(opportunities)
}

const handleCopyRecordLink = async (recordId: string | undefined) => {
Expand Down Expand Up @@ -221,6 +227,7 @@ export function OpportunitiesMain() {
setRecordColumns(
getRecordColumns(
icpRef.current,
allUsers,
updateOpportunityCallback,
handleOpenDrawerCallback
)
Expand All @@ -246,13 +253,13 @@ export function OpportunitiesMain() {
</div>

<div className="flex flex-col flex-1 overflow-hidden bg-white dark:bg-card rounded-lg border border-border">
{isPopulated && icp ? (
{isPopulated && icp && allUsers ? (
<>
<Sheet modal={false} open={sheetOpen}>
<DataTable
columns={recordColumns}
data={records}
filters={getFilters(icp)}
filters={getFilters(icp, allUsers)}
preSelectedFilters={preSelectedFilters}
defaultColumnVisibility={getDefaultColumnVisibility(icp)}
enableRowSelection={true}
Expand Down Expand Up @@ -307,22 +314,27 @@ export function OpportunitiesMain() {
</span>
</div>
</div>

{selectedRow !== null && (
<OpportunityStageList
opportunity={selectedRow}
updateOpportunity={updateOpportunityCallback}
/>
<>
<div className="flex flex-row items-center gap-2">
<OpportunityOwner
opportunity={selectedRow}
updateOpportunity={updateOpportunityCallback}
/>
<OpportunityStageList
opportunity={selectedRow}
updateOpportunity={updateOpportunityCallback}
/>
</div>
</>
)}
</div>
</TooltipProvider>

<div className="flex-1 overflow-y-auto card">
{selectedRow !== null && (
<OpportunityDrawer
opportunity={selectedRow}
updateOpportunity={updateOpportunityCallback}
icp={icp}
/>
<OpportunityDrawer opportunity={selectedRow} icp={icp} />
)}
</div>
</SheetContent>
Expand Down
33 changes: 1 addition & 32 deletions frontend/src/components/opportunities/OpportunityDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,3 @@
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import Image from "next/image"
import { timeAgo } from "@/utils/misc"
import { type Person } from "@/types/person"
import { Tabs, TabsList, TabsContent, TabsTrigger } from "@/components/ui/tabs"

import {
Divide,
ExternalLink,
Maximize2,
Users,
Factory,
MapPin,
Landmark,
Banknote,
Target,
Loader,
StickyNote,
ChevronRight,
CircleUserIcon,
Linkedin,
Mail,
} from "lucide-react"

import { type Icp } from "@/types/icp"
import { type Opportunity } from "@/types/opportunity"
import { OpportunityBrand } from "./OpportunityBrand"
Expand All @@ -32,13 +7,11 @@ import { Separator } from "@/components/ui/separator"

interface OpportunityDrawerProps {
opportunity: Opportunity
updateOpportunity: (updatedOpportunity: Opportunity) => void
icp: Icp | null
}

export function OpportunityDrawer({
opportunity,
updateOpportunity,
icp,
}: OpportunityDrawerProps) {
return (
Expand All @@ -47,11 +20,7 @@ export function OpportunityDrawer({
<div className="flex flex-col">
<OpportunityBrand opportunity={opportunity} />
<Separator />
<OpportunityTabs
opportunity={opportunity}
updateOpportunity={updateOpportunity}
icp={icp}
/>
<OpportunityTabs opportunity={opportunity} icp={icp} />
</div>
</div>
</>
Expand Down
22 changes: 12 additions & 10 deletions frontend/src/components/opportunities/OpportunityFull.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client"
import { useEffect, useState } from "react"

import TextEditor from "@/components/editor/editor"
import { type Icp } from "@/types/icp"
import { type Opportunity } from "@/types/opportunity"
Expand All @@ -22,6 +21,7 @@ import { Separator } from "@/components/ui/separator"

import { OpportunityStageList } from "./OpportunityStageList"
import { OpportunityTabs } from "./OpportunityTabs"
import { OpportunityOwner } from "./OpportunityOwner"

interface OpportunityFullProps {
opportunityId: string
Expand Down Expand Up @@ -100,19 +100,21 @@ export function OpportunityFull({ opportunityId }: OpportunityFullProps) {
<div className="text-sm text-zinc-400">
{opportunity.slug}
</div>
<OpportunityStageList
opportunity={opportunity}
updateOpportunity={updateOpportunity}
/>
<div className="flex flex-row gap-x-1 items-center">
<OpportunityOwner
opportunity={opportunity}
updateOpportunity={updateOpportunity}
/>
<OpportunityStageList
opportunity={opportunity}
updateOpportunity={updateOpportunity}
/>
</div>
</div>
<Separator />
<OpportunityBrand opportunity={opportunity} />
<Separator />
<OpportunityTabs
opportunity={opportunity}
updateOpportunity={updateOpportunity}
icp={icp}
/>
<OpportunityTabs opportunity={opportunity} icp={icp} />
</div>
</div>

Expand Down
145 changes: 145 additions & 0 deletions frontend/src/components/opportunities/OpportunityOwner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use client"
import { useEffect, useState } from "react"

import { CircleUserRound, Check, UserCircleIcon } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { toast } from "sonner"

import { User } from "@/types/user"
import { Opportunity } from "@/types/opportunity"
import { getNameInitials } from "@/utils/misc"
import { getUsers } from "@/utils/chapter/users"
import {
getOpportunity,
updateOpportunityOwner,
} from "@/utils/chapter/opportunity"

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu"

interface OpportunityOwnerProps {
opportunity: Opportunity
updateOpportunity: (updatedOpportunity: Opportunity) => void
}

export function OpportunityOwner({
opportunity,
updateOpportunity,
}: OpportunityOwnerProps) {
const [allUsers, setAllUsers] = useState<User[]>([])
const [selectedOwner, setSelectedOwner] = useState<User | null>(
opportunity.owner
)

const handleOwnerChange = async (newOwner: User | null) => {
try {
let newOwnerId: string | null = null

if (newOwner !== null) {
const user = allUsers.find((u) => u.id === newOwner.id)
if (!user) {
toast.error("Failed to set owner.")
return
}
newOwnerId = newOwner.id
}

const updatedOpportunity = await updateOpportunityOwner(
opportunity.id,
newOwnerId
)
setSelectedOwner(newOwner)
updateOpportunity(updatedOpportunity)
} catch (error: any) {
toast.error("Failed to update owner.", { description: error.toString() })
}
}

useEffect(() => {
setSelectedOwner(opportunity.owner)
}, [opportunity])

useEffect(() => {
const fetchUsers = async () => {
try {
const users = await getUsers()
setAllUsers(users)
} catch (error: any) {
toast.error("Failed to load users.", { description: error.toString() })
}
}
fetchUsers()
}, [])

return (
<>
<DropdownMenu>
<DropdownMenuTrigger className="flex flex-row items-center border border-border h-[25px] px-2 text-sm font-normal rounded-lg text-secondary-foreground hover:text-foreground">
{selectedOwner ? (
<>
<Avatar className="h-[15px] w-[15px] mr-1.5 rounded-lg">
<AvatarImage
src={selectedOwner.avatarUrl}
alt={selectedOwner.name}
/>
<AvatarFallback className="text-[8px]">
{getNameInitials(selectedOwner.name)}
</AvatarFallback>
</Avatar>
{selectedOwner.name}
</>
) : (
<>
<CircleUserRound size={15} className="mr-1.5" />
Assignee
</>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-[200px] font-normal"
>
{/* Render out the list of users as dropdown items, add an option at the top titled "no assignee" and if none is selected make that selected, else the selected user */}
<DropdownMenuLabel>Team members</DropdownMenuLabel>
<DropdownMenuSeparator />

<DropdownMenuCheckboxItem
onClick={() => handleOwnerChange(null)}
className=""
>
<div className="flex h-6 w-6 me-2 items-center justify-center">
<span className="flex">
<UserCircleIcon size={16} />
</span>
</div>
<span className="opacity-50">No assignee</span>
</DropdownMenuCheckboxItem>

{allUsers.map((user) => (
<DropdownMenuCheckboxItem
key={user.id}
onSelect={() => handleOwnerChange(user)}
checked={selectedOwner ? selectedOwner.id === user.id : false}
className="hover:bg-red-400 cursor-pointer"
>
<Avatar className="mr-2 h-6 w-6 rounded-lg">
<AvatarImage src={user.avatarUrl} alt={user.name} />
<AvatarFallback className="text-xs bg-muted">
{getNameInitials(user.name)}
</AvatarFallback>
</Avatar>
{user.name}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</>
)
}
Loading

0 comments on commit 779539f

Please sign in to comment.