From 26b5f1c1701d7db381c7bdc859c5a980d9d74ed0 Mon Sep 17 00:00:00 2001
From: MuhammadUmer44 <muhammadumerakhtar51@gmail.com>
Date: Sun, 29 Dec 2024 10:17:49 +0500
Subject: [PATCH 1/3] feat: add status calculation and display to bounty cards

---
 .../WorkSpacePlanner/BountyCard/index.tsx     | 24 +++++++++-
 src/store/bountyCard.ts                       | 46 +++++++++++++++++--
 src/store/interface.ts                        | 15 ++++++
 3 files changed, 79 insertions(+), 6 deletions(-)

diff --git a/src/people/WorkSpacePlanner/BountyCard/index.tsx b/src/people/WorkSpacePlanner/BountyCard/index.tsx
index 403f60f7..145cf7fe 100644
--- a/src/people/WorkSpacePlanner/BountyCard/index.tsx
+++ b/src/people/WorkSpacePlanner/BountyCard/index.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import styled from 'styled-components';
 import PropTypes from 'prop-types';
-import { BountyCard } from '../../../store/interface';
+import { BountyCard, BountyCardStatus } from '../../../store/interface';
 import { colors } from '../../../config';
 
 const truncate = (str: string, n: number) => (str.length > n ? `${str.substr(0, n - 1)}...` : str);
@@ -88,6 +88,22 @@ const RowB = styled.div`
   }
 `;
 
+const StatusText = styled.span<{ status?: BountyCardStatus }>`
+  color: ${({ status }: { status?: BountyCardStatus }): string => {
+    switch (status) {
+      case 'Paid':
+        return colors.light.statusPaid;
+      case 'Complete':
+        return colors.light.statusCompleted;
+      case 'Assigned':
+        return colors.light.statusAssigned;
+      default:
+        return colors.light.pureBlack;
+    }
+  }};
+  font-weight: 500;
+`;
+
 interface BountyCardProps extends BountyCard {
   onclick: (bountyId: string) => void;
 }
@@ -99,6 +115,7 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
   phase,
   assignee_img,
   workspace,
+  status,
   onclick
 }: BountyCardProps) => (
   <CardContainer onClick={() => onclick(id)}>
@@ -124,7 +141,9 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
       <span title={workspace?.name ?? 'No Workspace'}>
         {truncate(workspace?.name ?? 'No Workspace', 20)}
       </span>
-      <span className="last-span">Paid?</span>
+      <StatusText className="last-span" status={status}>
+        {status || 'Todo'}
+      </StatusText>
     </RowB>
   </CardContainer>
 );
@@ -142,6 +161,7 @@ BountyCardComponent.propTypes = {
   workspace: PropTypes.shape({
     name: PropTypes.string
   }) as PropTypes.Validator<BountyCard['workspace']>,
+  status: PropTypes.oneOf(['Todo', 'Assigned', 'Complete', 'Paid'] as BountyCardStatus[]),
   onclick: PropTypes.func.isRequired
 };
 
diff --git a/src/store/bountyCard.ts b/src/store/bountyCard.ts
index 50d38525..646b89f3 100644
--- a/src/store/bountyCard.ts
+++ b/src/store/bountyCard.ts
@@ -1,7 +1,7 @@
-import { makeAutoObservable, runInAction } from 'mobx';
+import { makeAutoObservable, runInAction, computed } from 'mobx';
 import { TribesURL } from 'config';
 import { useMemo } from 'react';
-import { BountyCard } from './interface';
+import { BountyCard, BountyCardStatus } from './interface';
 import { uiStore } from './ui';
 
 export class BountyCardStore {
@@ -29,6 +29,19 @@ export class BountyCardStore {
     }).toString();
   }
 
+  private calculateBountyStatus(bounty: BountyCard): BountyCardStatus {
+    if (bounty.paid) {
+      return 'Paid';
+    }
+    if (bounty.completed || bounty.payment_pending) {
+      return 'Complete';
+    }
+    if (bounty.assignee_img) {
+      return 'Assigned';
+    }
+    return 'Todo';
+  }
+
   loadWorkspaceBounties = async (): Promise<void> => {
     if (!this.currentWorkspaceId || !uiStore.meInfo?.tribe_jwt) {
       runInAction(() => {
@@ -63,9 +76,18 @@ export class BountyCardStore {
 
       runInAction(() => {
         if (this.pagination.currentPage === 1) {
-          this.bountyCards = data || [];
+          this.bountyCards = (data || []).map((bounty: BountyCard) => ({
+            ...bounty,
+            status: this.calculateBountyStatus(bounty)
+          }));
         } else {
-          this.bountyCards = [...this.bountyCards, ...(data || [])];
+          this.bountyCards = [
+            ...this.bountyCards,
+            ...(data || []).map((bounty: BountyCard) => ({
+              ...bounty,
+              status: this.calculateBountyStatus(bounty)
+            }))
+          ];
         }
         this.pagination.total = data?.length || 0;
       });
@@ -106,6 +128,22 @@ export class BountyCardStore {
 
     await this.loadWorkspaceBounties();
   };
+
+  @computed get todoItems() {
+    return this.bountyCards.filter((card: BountyCard) => card.status === 'Todo');
+  }
+
+  @computed get assignedItems() {
+    return this.bountyCards.filter((card: BountyCard) => card.status === 'Assigned');
+  }
+
+  @computed get completedItems() {
+    return this.bountyCards.filter((card: BountyCard) => card.status === 'Complete');
+  }
+
+  @computed get paidItems() {
+    return this.bountyCards.filter((card: BountyCard) => card.status === 'Paid');
+  }
 }
 
 export const useBountyCardStore = (workspaceId: string) =>
diff --git a/src/store/interface.ts b/src/store/interface.ts
index b5460c34..6f037772 100644
--- a/src/store/interface.ts
+++ b/src/store/interface.ts
@@ -466,6 +466,7 @@ export type ChatRole = 'user' | 'assistant';
 export type ChatStatus = 'sending' | 'sent' | 'error';
 export type ContextTagType = 'productBrief' | 'featureBrief' | 'schematic';
 export type ChatSource = 'user' | 'agent';
+export type BountyCardStatus = 'Todo' | 'Assigned' | 'Complete' | 'Paid';
 
 export interface ContextTag {
   type: ContextTagType;
@@ -513,3 +514,17 @@ export interface BountyCard {
   workspace: Workspace;
   assignee_img?: string;
 }
+
+export interface BountyCard {
+  id: string;
+  title: string;
+  features: Feature;
+  phase: Phase;
+  workspace: Workspace;
+  assignee_img?: string;
+  status?: BountyCardStatus;
+  paid?: boolean;
+  completed?: boolean;
+  payment_pending?: boolean;
+  assignee?: string;
+}

From 775f760fe264479b229f111061a38b8bd66e541a Mon Sep 17 00:00:00 2001
From: MuhammadUmer44 <muhammadumerakhtar51@gmail.com>
Date: Sun, 29 Dec 2024 10:50:00 +0500
Subject: [PATCH 2/3] fix unit test

---
 src/store/__test__/bountyCard.spec.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/store/__test__/bountyCard.spec.ts b/src/store/__test__/bountyCard.spec.ts
index 72cf0464..94287203 100644
--- a/src/store/__test__/bountyCard.spec.ts
+++ b/src/store/__test__/bountyCard.spec.ts
@@ -47,7 +47,7 @@ describe('BountyCardStore', () => {
 
       store = await waitFor(() => new BountyCardStore(mockWorkspaceId));
 
-      expect(store.bountyCards).toEqual(mockBounties);
+      expect(store.bountyCards).toEqual([{ ...mockBounties[0], status: 'Todo' }]);
       expect(store.loading).toBe(false);
       expect(store.error).toBeNull();
     });
@@ -81,7 +81,7 @@ describe('BountyCardStore', () => {
       await waitFor(() => store.switchWorkspace(newWorkspaceId));
       expect(store.currentWorkspaceId).toBe(newWorkspaceId);
       expect(store.pagination.currentPage).toBe(1);
-      expect(store.bountyCards).toEqual(mockBounties);
+      expect(store.bountyCards).toEqual([{ ...mockBounties[0], status: 'Todo' }]);
     });
 
     it('should not reload if workspace id is the same', async () => {

From 4e80319d42ee7e749aadd1d43fd2c89a8fbafda8 Mon Sep 17 00:00:00 2001
From: MuhammadUmer44 <muhammadumerakhtar51@gmail.com>
Date: Sun, 29 Dec 2024 11:14:14 +0500
Subject: [PATCH 3/3] test(bountyCard): add status calculation tests

---
 .../WorkSpacePlanner/BountyCard/index.tsx     |   2 +-
 src/store/__test__/bountyCard.spec.ts         | 126 ++++++++++++++++++
 2 files changed, 127 insertions(+), 1 deletion(-)

diff --git a/src/people/WorkSpacePlanner/BountyCard/index.tsx b/src/people/WorkSpacePlanner/BountyCard/index.tsx
index 145cf7fe..4a99d351 100644
--- a/src/people/WorkSpacePlanner/BountyCard/index.tsx
+++ b/src/people/WorkSpacePlanner/BountyCard/index.tsx
@@ -142,7 +142,7 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
         {truncate(workspace?.name ?? 'No Workspace', 20)}
       </span>
       <StatusText className="last-span" status={status}>
-        {status || 'Todo'}
+        {status}
       </StatusText>
     </RowB>
   </CardContainer>
diff --git a/src/store/__test__/bountyCard.spec.ts b/src/store/__test__/bountyCard.spec.ts
index 94287203..948eba22 100644
--- a/src/store/__test__/bountyCard.spec.ts
+++ b/src/store/__test__/bountyCard.spec.ts
@@ -113,4 +113,130 @@ describe('BountyCardStore', () => {
       await waitFor(() => store.loadNextPage());
     });
   });
+
+  describe('calculateBountyStatus', () => {
+    let store: BountyCardStore;
+
+    beforeEach(async () => {
+      store = await waitFor(() => new BountyCardStore(mockWorkspaceId));
+    });
+
+    it('should return "Paid" when bounty is paid', async () => {
+      const mockBounty = {
+        id: '1',
+        title: 'Test Bounty',
+        paid: true,
+        completed: true,
+        payment_pending: false,
+        assignee_img: 'test.jpg'
+      };
+
+      fetchStub.resolves({
+        ok: true,
+        json: async () => [mockBounty]
+      } as Response);
+
+      await store.loadWorkspaceBounties();
+      expect(store.bountyCards[0].status).toBe('Paid');
+    });
+
+    it('should return "Complete" when bounty is completed but not paid', async () => {
+      const mockBounty = {
+        id: '1',
+        title: 'Test Bounty',
+        paid: false,
+        completed: true,
+        payment_pending: false,
+        assignee_img: 'test.jpg'
+      };
+
+      fetchStub.resolves({
+        ok: true,
+        json: async () => [mockBounty]
+      } as Response);
+
+      await store.loadWorkspaceBounties();
+      expect(store.bountyCards[0].status).toBe('Complete');
+    });
+
+    it('should return "Complete" when payment is pending', async () => {
+      const mockBounty = {
+        id: '1',
+        title: 'Test Bounty',
+        paid: false,
+        completed: false,
+        payment_pending: true,
+        assignee_img: 'test.jpg'
+      };
+
+      fetchStub.resolves({
+        ok: true,
+        json: async () => [mockBounty]
+      } as Response);
+
+      await store.loadWorkspaceBounties();
+      expect(store.bountyCards[0].status).toBe('Complete');
+    });
+
+    it('should return "Assigned" when bounty has assignee but not completed or paid', async () => {
+      const mockBounty = {
+        id: '1',
+        title: 'Test Bounty',
+        paid: false,
+        completed: false,
+        payment_pending: false,
+        assignee_img: 'test.jpg'
+      };
+
+      fetchStub.resolves({
+        ok: true,
+        json: async () => [mockBounty]
+      } as Response);
+
+      await store.loadWorkspaceBounties();
+      expect(store.bountyCards[0].status).toBe('Assigned');
+    });
+
+    it('should return "Todo" when bounty has no assignee and is not completed or paid', async () => {
+      const mockBounty = {
+        id: '1',
+        title: 'Test Bounty',
+        paid: false,
+        completed: false,
+        payment_pending: false,
+        assignee_img: undefined
+      };
+
+      fetchStub.resolves({
+        ok: true,
+        json: async () => [mockBounty]
+      } as Response);
+
+      await store.loadWorkspaceBounties();
+      expect(store.bountyCards[0].status).toBe('Todo');
+    });
+
+    describe('computed status lists', () => {
+      it('should correctly filter bounties by status', async () => {
+        const mockBounties = [
+          { id: '1', title: 'Bounty 1', paid: true, completed: true, assignee_img: 'test.jpg' },
+          { id: '2', title: 'Bounty 2', completed: true, assignee_img: 'test.jpg' },
+          { id: '3', title: 'Bounty 3', assignee_img: 'test.jpg' },
+          { id: '4', title: 'Bounty 4' }
+        ];
+
+        fetchStub.resolves({
+          ok: true,
+          json: async () => mockBounties
+        } as Response);
+
+        await store.loadWorkspaceBounties();
+
+        expect(store.paidItems.length).toBe(1);
+        expect(store.completedItems.length).toBe(1);
+        expect(store.assignedItems.length).toBe(1);
+        expect(store.todoItems.length).toBe(1);
+      });
+    });
+  });
 });