Skip to content

Commit 628eedc

Browse files
committed
feat: new coge view and dc stuff
1 parent 6a63733 commit 628eedc

33 files changed

Lines changed: 2741 additions & 1275 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { useState } from 'react';
2+
import { Meta, StoryFn } from '@storybook/react';
3+
import { CRMCogeBoard } from './CogeBoard';
4+
import { BoardData, Deal, Customer, CRMStatus } from './types';
5+
import { mockUsers, mockCustomers, mockCategories } from './mockData';
6+
7+
const meta: Meta<typeof CRMCogeBoard> = {
8+
title: 'CRM/CogeBoard',
9+
tags: ['autodocs'],
10+
component: CRMCogeBoard,
11+
};
12+
13+
export default meta;
14+
type Story = StoryFn<typeof CRMCogeBoard>;
15+
16+
const Template: Story = (args) => {
17+
const [boardData, setBoardData] = useState<BoardData>(args.initialData);
18+
19+
const handleUpdateDeal = async (updatedDeal: Deal) => {
20+
console.log('Updating deal:', updatedDeal);
21+
setBoardData((prevData) => ({
22+
...prevData,
23+
deals: (prevData.deals ?? []).map((deal) =>
24+
deal.id === updatedDeal.id ? updatedDeal : deal
25+
),
26+
}));
27+
return updatedDeal;
28+
};
29+
30+
return (
31+
<CRMCogeBoard
32+
{...args}
33+
initialData={boardData}
34+
updateDeal={handleUpdateDeal}
35+
/>
36+
);
37+
};
38+
39+
export const Default: Story = Template.bind({});
40+
Default.args = {
41+
initialData: {
42+
deals: Array.from({ length: 13 }, (_, i) => ({
43+
id: i + 1,
44+
customer: mockCustomers[(i % mockCustomers.length)],
45+
value: Math.floor(Math.random() * 100000) + 5000,
46+
assignee: mockUsers[Math.floor(Math.random() * mockUsers.length)].name,
47+
status: ["Cold", "Qualified", "Proposal Made", "Won", "Lost"][Math.floor(Math.random() * 5)] as CRMStatus,
48+
gecoStatus: ["firm", "forecast", "other"][i % 3],
49+
categories: [mockCategories[(i % mockCategories.length)].id],
50+
dateLogged: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000).toISOString(),
51+
closureDate: new Date(Date.now() + Math.floor(Math.random() * 90) * 24 * 60 * 60 * 1000).toISOString(),
52+
comments: [],
53+
updatedAt: new Date().toISOString(),
54+
description: `This is a mock description for deal ${i + 1}. It's a ${["small", "medium", "large"][i % 3]} deal with high potential.`,
55+
})),
56+
customers: mockCustomers,
57+
users: mockUsers,
58+
categories: mockCategories,
59+
},
60+
61+
addComment: async (dealId, comment) => {
62+
console.log("Adding comment to deal:", dealId, comment);
63+
return { ...comment, id: "new-comment-id" };
64+
},
65+
};
66+
67+
export const NoDeals: Story = Template.bind({});
68+
NoDeals.args = {
69+
...Default.args,
70+
initialData: {
71+
...Default.args.initialData,
72+
deals: [],
73+
},
74+
};
75+

src/components/CRM/CogeBoard.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use client'
2+
3+
import React, { useState, useCallback } from 'react'
4+
import type { BoardData, Deal, gecoStatus, CRMStatus, Customer, EditableDeal, PartialComment } from './types'
5+
import { DealDetails } from './DealDetails'
6+
import { KanbanColumn } from './KanbanColumn'
7+
import {
8+
DragDropContext,
9+
type DropResult,
10+
} from '@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-migration'
11+
12+
const gcstatuses: gecoStatus[] = ['firm', 'forecast', 'other']
13+
const statuses: CRMStatus[] = ['Cold', 'Qualified', 'Proposal Made', 'SoW Submitted', 'Won', 'Lost']
14+
15+
type CogeBoardProps = {
16+
initialData: BoardData
17+
// onDragEnd: (result: DropResult) => void
18+
// addNewDeal: (newDeal: Deal) => Promise<{ success: boolean; errors?: Record<string, string> }>;
19+
updateDeal: (updatedDeal: Partial<EditableDeal>) => Promise<{ success: boolean; errors?: Record<string, string> }>;
20+
addComment: (dealId: string, comment: PartialComment) => Promise<{ success: boolean; errors?: Record<string, string> }>;
21+
// addNewCustomer: (newCustomer: Partial<Customer>) => Promise<{ success: boolean; errors?: Record<string, string> }>;
22+
}
23+
24+
export function CRMCogeBoard({
25+
initialData,
26+
// addNewDeal,
27+
updateDeal,
28+
addComment,
29+
// addNewCustomer,
30+
}: CogeBoardProps) {
31+
const [selectedDeal, setSelectedDeal] = useState<Deal | null>(null)
32+
// const [boardData, setBoardData] = useState<BoardData>(initialData)
33+
34+
const getColumnDeals = (status: gecoStatus) => {
35+
return (initialData.deals ?? [])
36+
.filter((deal) => deal.gecoStatus === status)
37+
.filter((deal) => deal.status !== 'Lost' && deal.status !== 'Cold')
38+
}
39+
40+
const calculateColumnValue = (deals: Deal[]) => {
41+
return deals.reduce((sum, deal) => sum + (deal.value || 0), 0)
42+
}
43+
44+
const calculateWeightedValue = (deals: Deal[], status: CRMStatus | gecoStatus) => {
45+
const weightMap: Record<CRMStatus, number> = {
46+
Cold: 0,
47+
Qualified: 0.2,
48+
'Proposal Made': 0.5,
49+
'SoW Submitted': 0.8,
50+
Won: 1,
51+
Lost: 0,
52+
}
53+
return deals.reduce((sum, deal) => sum + (deal.value || 0) * (weightMap[deal.status] ?? 0), 0)
54+
}
55+
56+
const onDragEnd = useCallback(
57+
(result: DropResult) => {
58+
const { source, destination } = result
59+
// console.log('onDragEnd', result, destination)
60+
if (!destination) {
61+
return
62+
}
63+
64+
if (source.droppableId === destination.droppableId && source.index === destination.index) {
65+
return
66+
}
67+
68+
const sourceStatus = source.droppableId as gecoStatus
69+
const destinationStatus = destination.droppableId as gecoStatus
70+
// console.log('Moving from ', sourceStatus, 'to', destinationStatus)
71+
// find the deal with id source.index
72+
//
73+
let movedDeal = initialData.deals?.filter((deal) => deal.id === source.index)[0]
74+
// console.log('deal', movedDeal)
75+
76+
const updatedDeals = [...(initialData.deals ?? [])]
77+
// const [movedDeal] = updatedDeals.splice(source.index, 1)
78+
if (movedDeal) {
79+
movedDeal.gecoStatus = destinationStatus
80+
// updatedDeals.splice(destination.index, 0, movedDeal)
81+
82+
// Update the deal using the updateDeal function
83+
updateDeal(movedDeal as EditableDeal)
84+
}
85+
},
86+
[updateDeal],
87+
)
88+
89+
return (
90+
<DragDropContext onDragEnd={onDragEnd}>
91+
<div className="p-4 h-full overflow-auto">
92+
{/* <h1 className="text-3xl font-bold mb-8">Cortex Sales Pipeline</h1> */}
93+
<div className="flex space-x-4 pb-4">
94+
{gcstatuses
95+
96+
.map((status) => {
97+
const deals = getColumnDeals(status)
98+
return (
99+
<KanbanColumn
100+
key={status}
101+
status={status}
102+
deals={deals}
103+
customers={initialData.customers ?? []}
104+
users={initialData.users}
105+
categories={initialData.categories ?? []}
106+
onDealClick={setSelectedDeal}
107+
calculateColumnValue={calculateColumnValue}
108+
calculateWeightedValue={calculateWeightedValue}
109+
compact={true}
110+
/>
111+
)
112+
})}
113+
</div>
114+
115+
{selectedDeal && (
116+
<DealDetails
117+
deal={selectedDeal}
118+
users={initialData.users}
119+
customer={selectedDeal.customer as Customer}
120+
categories={initialData.categories ?? []}
121+
onClose={() => setSelectedDeal(null)}
122+
onSave={updateDeal}
123+
onAddComment={(comment) => addComment(selectedDeal.id.toString(), comment)}
124+
/>
125+
)}
126+
</div>
127+
</DragDropContext>
128+
)
129+
}

src/components/CRM/DealCard.stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export const Default: Story = {
2222
},
2323
}
2424

25+
export const Compact: Story = {
26+
args: {
27+
...Default.args,
28+
compact: true,
29+
},
30+
}
31+
2532
export const LargeDeal: Story = {
2633
args: {
2734
...Default.args,

src/components/CRM/DealCard.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,39 @@ type DealCardProps = {
99
customer: Partial<Customer> | undefined
1010
categories: DealCategory[]
1111
onClick: () => void
12+
compact?: boolean
1213
}
1314

14-
export function DealCard({ deal, customer, categories, onClick }: DealCardProps) {
15+
export function DealCard({ deal, customer, categories, onClick, compact = false }: DealCardProps) {
1516
const propositionCategory = categories.find((c) => c.type === "proposition" && (deal.categories ?? []).includes(c.id))
1617

18+
if (compact) {
19+
return (
20+
<Card
21+
className="mb-1 cursor-pointer hover:shadow-md transition-shadow border-l-4 border-l-accent"
22+
onClick={onClick}
23+
>
24+
<CardHeader className="pb-1 pt-2">
25+
<CardTitle className="text-sm font-semibold text-foreground">{customer?.name}</CardTitle>
26+
<p className="text-sm text-gray-500 mt-1">{deal.description}</p>
27+
28+
</CardHeader>
29+
<CardContent className="text-xs text-gray-500 pb-2">
30+
<div className="flex justify-between items-center mb-1">
31+
{/* <div className="flex items-center">
32+
<UserIcon className="w-3 h-3 mr-1" />
33+
<span>{(deal.assignee as User).name}</span>
34+
</div> */}
35+
<div className="flex items-center text-sm font-bold text-green-600">
36+
<span>£{deal.value.toLocaleString()}</span>
37+
</div>
38+
</div>
39+
40+
</CardContent>
41+
</Card>
42+
)
43+
}
44+
1745
return (
1846
<Card
1947
className="mb-4 cursor-pointer hover:shadow-md transition-shadow border-l-4 border-l-accent"

src/components/CRM/DealDetails.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const mockDeal: Deal = {
4646
value: 100000,
4747
assignee: { id: 1, name: 'John Doe'},
4848
status: 'Qualified',
49+
gecoStatus: 'firm',
4950
categories: mockCategories.slice(0, 3), // Pass in 3 of the mockCategories
5051
dateLogged: '2023-01-01T00:00:00.000Z',
5152
closureDate: '2023-12-31T00:00:00.000Z',

src/components/CRM/DealDetails.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,38 @@ try {
324324
})}
325325
</div>
326326
)}
327+
</div>
328+
<div className="grid gap-2 m-1">
329+
<Label className="text-sm font-medium text-accent">COGE Status</Label>
330+
{editingField === 'gecoStatus' ? (
331+
<Select
332+
value={editedDeal.gecoStatus ?? undefined}
333+
onValueChange={(value) => {
334+
handleChange('gecoStatus', value)
335+
setEditingField(null)
336+
}}
337+
>
338+
<SelectTrigger>
339+
<SelectValue placeholder="Select status" />
340+
</SelectTrigger>
341+
<SelectContent>
342+
{['firm', 'forecast', 'other'].map((status) => (
343+
<SelectItem key={status} value={status}>
344+
{status.charAt(0).toUpperCase() + status.slice(1)}
345+
</SelectItem>
346+
))}
347+
</SelectContent>
348+
</Select>
349+
) : (
350+
<div
351+
className="cursor-pointer p-1 border border-background hover:border-accent rounded"
352+
onClick={() => setEditingField('gecoStatus')}
353+
>
354+
{editedDeal.gecoStatus
355+
? editedDeal.gecoStatus.charAt(0).toUpperCase() + editedDeal.gecoStatus.slice(1)
356+
: 'N/A'}
357+
</div>
358+
)}
327359
</div>
328360
</div>
329361

src/components/CRM/KanbanBoard.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import React, { useState, useCallback } from 'react'
4-
import type { BoardData, Deal, CRMStatus, Customer, EditableDeal, PartialComment } from './types'
4+
import type { BoardData, Deal, CRMStatus, Customer, EditableDeal, PartialComment , gecoStatus} from './types'
55
import { DealDetails } from './DealDetails'
66
import { KanbanColumn } from './KanbanColumn'
77
import {
@@ -38,16 +38,16 @@ export function CRMKanbanBoard({
3838
return deals.reduce((sum, deal) => sum + (deal.value || 0), 0)
3939
}
4040

41-
const calculateWeightedValue = (deals: Deal[], status: CRMStatus) => {
42-
const weightMap: { [key in CRMStatus]: number } = {
43-
Cold: 0.2,
44-
Qualified: 0.4,
45-
'Proposal Made': 0.6,
46-
'SoW Submitted': 0.7,
41+
const calculateWeightedValue = (deals: Deal[], status: CRMStatus | gecoStatus ) => {
42+
const weightMap = {
43+
Cold: 0,
44+
Qualified: 0.2,
45+
'Proposal Made': 0.5,
46+
'SoW Submitted': 0.8,
4747
Won: 1,
4848
Lost: 0,
4949
}
50-
return deals.reduce((sum, deal) => sum + (deal.value || 0) * weightMap[status], 0)
50+
return deals.reduce((sum, deal) => sum + (deal.value || 0) * weightMap[status as CRMStatus], 0)
5151
}
5252

5353
const onDragEnd = useCallback(
@@ -89,7 +89,7 @@ export function CRMKanbanBoard({
8989
{/* <h1 className="text-3xl font-bold mb-8">Cortex Sales Pipeline</h1> */}
9090
<div className="flex space-x-4 pb-4">
9191
{statuses
92-
.filter((status) => status !== 'Won' && status !== 'Lost')
92+
.filter((status) => status !== 'Lost')
9393
.map((status) => {
9494
const deals = getColumnDeals(status)
9595
return (

0 commit comments

Comments
 (0)