diff --git a/db/db.go b/db/db.go index 47aa4bfb3..b09dc5d01 100644 --- a/db/db.go +++ b/db/db.go @@ -1104,6 +1104,12 @@ func (db database) GetOrganizationBudget(org_uuid string) BountyBudget { return ms } +func (db database) GetOrganizationBudgetHistory(org_uuid string) []BudgetHistoryData { + budgetHistory := []BudgetHistoryData{} + db.db.Raw(`SELECT budget.id, budget.org_uuid, budget.amount, budget.created, budget.updated, budget.payment_type, budget.status, budget.sender_pub_key, sender.unique_name AS sender_name FROM public.budget_histories AS budget LEFT OUTER JOIN public.people AS sender ON budget.sender_pub_key = sender.owner_pub_key WHERE budget.org_uuid = '` + org_uuid + `' ORDER BY budget.created DESC`).Find(&budgetHistory) + return budgetHistory +} + func (db database) AddAndUpdateBudget(budget BudgetStoreData) BudgetHistory { created := budget.Created org_uuid := budget.OrgUuid diff --git a/db/structs.go b/db/structs.go index 58d8a7be3..d577cd202 100644 --- a/db/structs.go +++ b/db/structs.go @@ -445,10 +445,11 @@ type BountyBudget struct { } type BudgetInvoiceRequest struct { - Amount uint `json:"amount"` - SenderPubKey string `json:"sender_pubkey"` - OrgUuid string `json:"org_uuid"` - Websocket_token string `json:"websocket_token,omitempty"` + Amount uint `json:"amount"` + SenderPubKey string `json:"sender_pubkey"` + OrgUuid string `json:"org_uuid"` + PaymentType BudgetPaymentType `json:"payment_type,omitempty"` + Websocket_token string `json:"websocket_token,omitempty"` } type BudgetStoreData struct { @@ -460,14 +461,27 @@ type BudgetStoreData struct { Created *time.Time `json:"created"` } +type BudgetPaymentType string + +const ( + Add BudgetPaymentType = "add" + Deposit BudgetPaymentType = "deposit" +) + type BudgetHistory struct { - ID uint `json:"id"` - OrgUuid string `json:"org_uuid"` - Amount uint `json:"amount"` - SenderPubKey string `json:"sender_pubkey"` - Created *time.Time `json:"created"` - Updated *time.Time `json:"updated"` - Status bool `json:"status"` + ID uint `json:"id"` + OrgUuid string `json:"org_uuid"` + Amount uint `json:"amount"` + SenderPubKey string `json:"sender_pubkey"` + Created *time.Time `json:"created"` + Updated *time.Time `json:"updated"` + Status bool `json:"status"` + PaymentType BudgetPaymentType `json:"payment_type"` +} + +type BudgetHistoryData struct { + BudgetHistory + SenderName string `json:"sender_name"` } type PaymentHistory struct { diff --git a/frontend/app/src/components/form/style.ts b/frontend/app/src/components/form/style.ts index 9a9e14c14..4a5fbb006 100644 --- a/frontend/app/src/components/form/style.ts +++ b/frontend/app/src/components/form/style.ts @@ -18,6 +18,17 @@ export const Wrap = styled.div` min-width: 230px; `; +export const OrgWrap = styled.div` + padding: ${(p: any) => (p?.newDesign ? '28px 0px' : '30px 20px')}; + margin-bottom: ${(p: any) => !p?.newDesign && '100px'}; + display: flex; + height: inherit; + flex-direction: column; + align-content: center; + min-width: 500px; + max-width: auto; +`; + interface bottomButtonProps { assigneeName?: string; color?: any; diff --git a/frontend/app/src/helpers/helpers.ts b/frontend/app/src/helpers/helpers.ts index ccef38d43..bff15426f 100644 --- a/frontend/app/src/helpers/helpers.ts +++ b/frontend/app/src/helpers/helpers.ts @@ -167,6 +167,8 @@ export const userHasRole = (bountyRoles: any[], userRoles: any[], role: Roles): }; export const toCapitalize = (word: string): string => { + if(!word.length) return word; + const wordString = word.split(' '); const capitalizeStrings = wordString.map((w: string) => w[0].toUpperCase() + w.slice(1)); diff --git a/frontend/app/src/people/widgetViews/OrganizationDetails.tsx b/frontend/app/src/people/widgetViews/OrganizationDetails.tsx index 9b6119e90..ebebbe0d7 100644 --- a/frontend/app/src/people/widgetViews/OrganizationDetails.tsx +++ b/frontend/app/src/people/widgetViews/OrganizationDetails.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { useStores } from 'store'; -import { Wrap } from 'components/form/style'; +import { OrgWrap, Wrap } from 'components/form/style'; import { EuiGlobalToastList } from '@elastic/eui'; import { InvoiceForm, InvoiceInput, InvoiceLabel } from 'people/utils/style'; import moment from 'moment'; @@ -10,7 +10,7 @@ import { Button, IconButton } from 'components/common'; import { useIsMobile } from 'hooks/uiHooks'; import { Formik } from 'formik'; import { FormField, validator } from 'components/form/utils'; -import { BountyRoles, Organization, PaymentHistory, Person } from 'store/main'; +import { BountyRoles, BudgetHistory, Organization, PaymentHistory, Person } from 'store/main'; import MaterialIcon from '@material/react-material-icon'; import { userHasRole } from 'helpers'; import { Modal } from '../../components/common'; @@ -153,9 +153,11 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und const [isOpenRoles, setIsOpenRoles] = useState(false); const [isOpenBudget, setIsOpenBudget] = useState(false); const [isOpenHistory, setIsOpenHistory] = useState(false); + const [isOpenBudgetHistory, setIsOpenBudgetHistory] = useState(false); const [usersCount, setUsersCount] = useState(0); const [orgBudget, setOrgBudget] = useState(0); const [paymentsHistory, setPaymentsHistory] = useState([]); + const [budgetsHistory, setBudgetsHistory] = useState([]); const [disableFormButtons, setDisableFormButtons] = useState(false); const [users, setUsers] = useState([]); const [user, setUser] = useState(); @@ -260,6 +262,11 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und setPaymentsHistory(paymentHistories); }, [main]); + const getBudgetHistory = useCallback(async () => { + const budgetHistories = await main.getBudgettHistories(uuid); + setBudgetsHistory(budgetHistories); + }, [main]); + const generateInvoice = async () => { const token = ui.meInfo?.websocketToken; if (token) { @@ -267,7 +274,8 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und amount: amount, sender_pubkey: ui.meInfo?.owner_pubkey ?? '', org_uuid: uuid, - websocket_token: token + websocket_token: token, + payment_type: 'deposit' }); setLnInvoice(data.response.invoice); @@ -296,6 +304,10 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und setIsOpenHistory(false); }; + const closeBudgetHistoryHandler = () => { + setIsOpenBudgetHistory(false); + }; + const onSubmit = async (body: any) => { setIsLoading(true); @@ -359,6 +371,7 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und // get new organization budget getOrganizationBudget(); + getBudgetHistory(); closeBudgetHandler(); } }; @@ -369,12 +382,14 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und getBountyRoles(); getOrganizationBudget(); getPaymentsHistory(); + getBudgetHistory(); }, [ getOrganizationUsers, getOrganizationUsersCount, getBountyRoles, getOrganizationBudget, - getPaymentsHistory + getPaymentsHistory, + getBudgetHistory ]); useEffect(() => { @@ -429,7 +444,10 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und /> )} {(isOrganizationAdmin || userHasRole(bountyRoles, userRoles, 'VIEW REPORT')) && ( - setIsOpenHistory(true)}>View history + <> + setIsOpenBudgetHistory(true)}>Budget history + setIsOpenHistory(true)}>Payment history + )} @@ -545,8 +563,8 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und style={ item.name === 'github_description' && !values.ticket_url ? { - display: 'none' - } + display: 'none' + } : undefined } /> @@ -710,7 +728,7 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und borderRadius: '50%' }} > - + Payment history @@ -730,7 +748,58 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und ))}
-
+ + + )} + {isOpenBudgetHistory && ( + + + Budget history + + + + + + + + + + + + {budgetsHistory.map((b: BudgetHistory, i: number) => ( + + + + + + + + ))} + +
SenderAmountTypeStatusDate
{b.sender_name}{b.amount} sats{b.payment_type}{b.status ? 'settled' : 'peending'}{moment(b.created).fromNow()}
+
)} diff --git a/frontend/app/src/people/widgetViews/OrganizationView.tsx b/frontend/app/src/people/widgetViews/OrganizationView.tsx index 9b1c7ab50..e9fbb3dd5 100644 --- a/frontend/app/src/people/widgetViews/OrganizationView.tsx +++ b/frontend/app/src/people/widgetViews/OrganizationView.tsx @@ -82,7 +82,7 @@ const Organizations = (props: { person: Person }) => { const isMobile = useIsMobile(); const config = widgetConfigs['organizations']; const formRef = useRef(null); - const isMyProfile = ui?.meInfo?.pubkey == props.person.owner_pubkey; + const isMyProfile = ui?.meInfo?.pubkey == props?.person?.owner_pubkey; const schema = [...config.schema]; @@ -247,8 +247,8 @@ const Organizations = (props: { person: Person }) => { style={ item.name === 'github_description' && !values.ticket_url ? { - display: 'none' - } + display: 'none' + } : undefined } /> diff --git a/frontend/app/src/store/main.ts b/frontend/app/src/store/main.ts index f65b796b1..bcc056a19 100644 --- a/frontend/app/src/store/main.ts +++ b/frontend/app/src/store/main.ts @@ -133,6 +133,18 @@ export interface PaymentHistory { created: string; } +export interface BudgetHistory { + id: number; + amount: number; + org_uuid: string; + payment_type: string; + created: string; + updated: string; + sender_pub_key: string; + sender_name: string; + status: boolean; +} + export interface PersonOffer { person: PersonFlex; title: string; @@ -1541,6 +1553,7 @@ export class MainStore { org_uuid: string; sender_pubkey: string; websocket_token: string; + payment_type: string; }): Promise { try { const data = await api.post( @@ -1549,7 +1562,8 @@ export class MainStore { amount: body.amount, org_uuid: body.org_uuid, sender_pubkey: body.sender_pubkey, - websocket_token: body.websocket_token + websocket_token: body.websocket_token, + payment_type: body.payment_type }, { 'Content-Type': 'application/json' @@ -1815,6 +1829,7 @@ export class MainStore { return r.json(); } catch (e) { + console.log('Error getOrganizationBudget', e); return false; } } @@ -1839,6 +1854,7 @@ export class MainStore { return r; } catch (e) { + console.log('Error makeBountyPayment', e); return false; } } @@ -1858,6 +1874,27 @@ export class MainStore { return r.json(); } catch (e) { + console.log('Error getPaymentHistories', e); + return []; + } + } + + async getBudgettHistories(uuid: string): Promise { + try { + if (!uiStore.meInfo) return []; + const info = uiStore.meInfo; + const r: any = await fetch(`${TribesURL}/organizations/budget/history/${uuid}`, { + method: 'GET', + mode: 'cors', + headers: { + 'x-jwt': info.tribe_jwt, + 'Content-Type': 'application/json' + } + }); + + return r.json(); + } catch (e) { + console.log('Error gettHistories', e); return []; } } diff --git a/handlers/organizations.go b/handlers/organizations.go index dd9a18356..076d71c3d 100644 --- a/handlers/organizations.go +++ b/handlers/organizations.go @@ -374,6 +374,15 @@ func GetOrganizationBudget(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(organizationBudget) } +func GetOrganizationBudgetHistory(w http.ResponseWriter, r *http.Request) { + uuid := chi.URLParam(r, "uuid") + // get the organization budget + organizationBudget := db.DB.GetOrganizationBudgetHistory(uuid) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(organizationBudget) +} + func GetPaymentHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) diff --git a/handlers/tribes.go b/handlers/tribes.go index 57027dd5e..e44c12381 100644 --- a/handlers/tribes.go +++ b/handlers/tribes.go @@ -556,6 +556,7 @@ func GenerateBudgetInvoice(w http.ResponseWriter, r *http.Request) { var budgetHistoryData = db.BudgetHistory{ Amount: invoice.Amount, OrgUuid: invoice.OrgUuid, + PaymentType: invoice.PaymentType, SenderPubKey: invoice.SenderPubKey, Created: &now, Updated: &now, diff --git a/routes/organizations.go b/routes/organizations.go index 2f85dfb74..9c0edffbe 100644 --- a/routes/organizations.go +++ b/routes/organizations.go @@ -30,6 +30,7 @@ func OrganizationRoutes() chi.Router { r.Get("/users/role/{uuid}/{user}", handlers.GetUserRoles) r.Get("/user", handlers.GetUserOrganizations) r.Get("/budget/{uuid}", handlers.GetOrganizationBudget) + r.Get("/budget/history/{uuid}", handlers.GetOrganizationBudgetHistory) r.Get("/payments/{uuid}", handlers.GetPaymentHistory) })