feat: init payments config backend & dash frontend

This commit is contained in:
swve 2024-09-26 19:07:30 +02:00
parent 96e453a4de
commit deba63cc15
15 changed files with 774 additions and 14 deletions

View file

@ -30,7 +30,7 @@ class CollectionUpdate(CollectionBase):
courses: Optional[list] courses: Optional[list]
name: Optional[str] name: Optional[str]
public: Optional[bool] public: Optional[bool]
description: Optional[str] description: Optional[str] = ""
class CollectionRead(CollectionBase): class CollectionRead(CollectionBase):

View file

@ -0,0 +1,49 @@
from datetime import datetime
from enum import Enum
from typing import Literal, Optional
from pydantic import BaseModel
from sqlalchemy import JSON
from sqlmodel import Field, SQLModel, Column, BigInteger, ForeignKey
class StripeProviderConfig(BaseModel):
stripe_key: str = ""
stripe_secret_key: str = ""
stripe_webhook_secret: str = ""
class PaymentProviderEnum(str, Enum):
STRIPE = "stripe"
class PaymentsConfigBase(SQLModel):
enabled: bool = False
provider: PaymentProviderEnum = PaymentProviderEnum.STRIPE
provider_config: dict = Field(default={}, sa_column=Column(JSON))
class PaymentsConfig(PaymentsConfigBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
creation_date: datetime = Field(default=datetime.now())
update_date: datetime = Field(default=datetime.now())
class PaymentsConfigCreate(PaymentsConfigBase):
pass
class PaymentsConfigUpdate(PaymentsConfigBase):
enabled: Optional[bool] = False
provider_config: Optional[dict] = None
class PaymentsConfigRead(PaymentsConfigBase):
id: int
org_id: int
creation_date: datetime
update_date: datetime
class PaymentsConfigDelete(SQLModel):
id: int

View file

@ -5,7 +5,7 @@ from src.routers import dev, trail, users, auth, orgs, roles
from src.routers.ai import ai from src.routers.ai import ai
from src.routers.courses import chapters, collections, courses, assignments from src.routers.courses import chapters, collections, courses, assignments
from src.routers.courses.activities import activities, blocks from src.routers.courses.activities import activities, blocks
from src.routers.ee import cloud_internal from src.routers.ee import cloud_internal, payments
from src.routers.install import install from src.routers.install import install
from src.services.dev.dev import isDevModeEnabledOrRaise from src.services.dev.dev import isDevModeEnabledOrRaise
from src.services.install.install import isInstallModeEnabled from src.services.install.install import isInstallModeEnabled
@ -32,6 +32,7 @@ v1_router.include_router(
) )
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"]) v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
v1_router.include_router(ai.router, prefix="/ai", tags=["ai"]) v1_router.include_router(ai.router, prefix="/ai", tags=["ai"])
v1_router.include_router(payments.router, prefix="/payments", tags=["payments"])
if os.environ.get("CLOUD_INTERNAL_KEY"): if os.environ.get("CLOUD_INTERNAL_KEY"):
v1_router.include_router( v1_router.include_router(

View file

@ -0,0 +1,53 @@
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.payments.payments import PaymentsConfig, PaymentsConfigBase, PaymentsConfigCreate, PaymentsConfigRead, PaymentsConfigUpdate
from src.db.users import PublicUser
from src.security.auth import get_current_user
from src.services.payments.payments import (
create_payments_config,
get_payments_config,
update_payments_config,
delete_payments_config,
)
router = APIRouter()
@router.post("/{org_id}/config")
async def api_create_payments_config(
request: Request,
org_id: int,
payments_config: PaymentsConfigCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> PaymentsConfig:
return await create_payments_config(request, org_id, payments_config, current_user, db_session)
@router.get("/{org_id}/config")
async def api_get_payments_config(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> list[PaymentsConfigRead]:
return await get_payments_config(request, org_id, current_user, db_session)
@router.put("/{org_id}/config")
async def api_update_payments_config(
request: Request,
org_id: int,
payments_config: PaymentsConfigUpdate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> PaymentsConfig:
return await update_payments_config(request, org_id, payments_config, current_user, db_session)
@router.delete("/{org_id}/config")
async def api_delete_payments_config(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
await delete_payments_config(request, org_id, current_user, db_session)
return {"message": "Payments config deleted successfully"}

View file

@ -0,0 +1,127 @@
from typing import Optional
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from src.db.payments.payments import (
PaymentsConfig,
PaymentsConfigCreate,
PaymentsConfigUpdate,
PaymentsConfigRead,
)
from src.db.users import PublicUser, AnonymousUser
from src.db.organizations import Organization
from src.services.orgs.orgs import rbac_check
async def create_payments_config(
request: Request,
org_id: int,
payments_config: PaymentsConfigCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> PaymentsConfig:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Check if payments config already exists for this organization
statement = select(PaymentsConfig).where(PaymentsConfig.org_id == org_id)
existing_config = db_session.exec(statement).first()
if existing_config:
raise HTTPException(
status_code=409,
detail="Payments config already exists for this organization",
)
# Create new payments config
new_config = PaymentsConfig(**payments_config.model_dump(), org_id=org_id)
db_session.add(new_config)
db_session.commit()
db_session.refresh(new_config)
return new_config
async def get_payments_config(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> list[PaymentsConfigRead]:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
# Get payments config
statement = select(PaymentsConfig).where(PaymentsConfig.org_id == org_id)
configs = db_session.exec(statement).all()
return [PaymentsConfigRead.model_validate(config) for config in configs]
async def update_payments_config(
request: Request,
org_id: int,
payments_config: PaymentsConfigUpdate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> PaymentsConfig:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Get existing payments config
statement = select(PaymentsConfig).where(PaymentsConfig.org_id == org_id)
config = db_session.exec(statement).first()
if not config:
raise HTTPException(status_code=404, detail="Payments config not found")
# Update config
for key, value in payments_config.model_dump().items():
setattr(config, key, value)
db_session.add(config)
db_session.commit()
db_session.refresh(config)
return config
async def delete_payments_config(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> None:
# Check if organization exists
statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(statement).first()
if not org:
raise HTTPException(status_code=404, detail="Organization not found")
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
# Get existing payments config
statement = select(PaymentsConfig).where(PaymentsConfig.org_id == org_id)
config = db_session.exec(statement).first()
if not config:
raise HTTPException(status_code=404, detail="Payments config not found")
# Delete config
db_session.delete(config)
db_session.commit()

View file

@ -1,8 +1,8 @@
'use client' 'use client'
import '@styles/globals.css' import '@styles/globals.css'
import { Menu } from '@components/Objects/Menu/Menu'
import { SessionProvider } from 'next-auth/react' import { SessionProvider } from 'next-auth/react'
import Watermark from '@components/Watermark' import Watermark from '@components/Watermark'
import { OrgMenu } from '@components/Objects/Menus/OrgMenu/OrgMenu'
export default function RootLayout({ export default function RootLayout({
children, children,
@ -14,7 +14,7 @@ export default function RootLayout({
return ( return (
<> <>
<SessionProvider> <SessionProvider>
<Menu orgslug={params?.orgslug}></Menu> <OrgMenu orgslug={params?.orgslug}></OrgMenu>
{children} {children}
<Watermark /> <Watermark />
</SessionProvider> </SessionProvider>

View file

@ -0,0 +1,139 @@
'use client'
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { CreditCard, Settings, Repeat, BookOpen, Users, DollarSign } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import PaymentsConfigurationPage from '@components/Dashboard/Payments/PaymentsConfigurationPage'
export type PaymentsParams = {
subpage: string
orgslug: string
}
function PaymentsPage({ params }: { params: PaymentsParams }) {
const session = useLHSession() as any
const org = useOrg() as any
const [selectedSubPage, setSelectedSubPage] = useState(params.subpage || 'general')
const [H1Label, setH1Label] = useState('')
const [H2Label, setH2Label] = useState('')
useEffect(() => {
handleLabels()
}, [selectedSubPage])
function handleLabels() {
if (selectedSubPage === 'general') {
setH1Label('Payments')
setH2Label('Overview of your payment settings and transactions')
}
if (selectedSubPage === 'configuration') {
setH1Label('Payment Configuration')
setH2Label('Set up and manage your payment gateway')
}
if (selectedSubPage === 'subscriptions') {
setH1Label('Subscriptions')
setH2Label('Manage your subscription plans')
}
if (selectedSubPage === 'paid-courses') {
setH1Label('Paid Courses')
setH2Label('Manage your paid courses and pricing')
}
if (selectedSubPage === 'customers') {
setH1Label('Customers')
setH2Label('View and manage your customer information')
}
}
return (
<div className="h-full w-full bg-[#f8f8f8]">
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
<BreadCrumbs type="payments" />
<div className="my-2 py-3">
<div className="w-100 flex flex-col space-y-1">
<div className="pt-3 flex font-bold text-4xl tracking-tighter">
{H1Label}
</div>
<div className="flex font-medium text-gray-400 text-md">
{H2Label}{' '}
</div>
</div>
</div>
<div className="flex space-x-5 font-black text-sm">
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/general')}
icon={<CreditCard size={16} />}
label="General"
isActive={selectedSubPage === 'general'}
onClick={() => setSelectedSubPage('general')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/configuration')}
icon={<Settings size={16} />}
label="Configuration"
isActive={selectedSubPage === 'configuration'}
onClick={() => setSelectedSubPage('configuration')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/subscriptions')}
icon={<Repeat size={16} />}
label="Subscriptions"
isActive={selectedSubPage === 'subscriptions'}
onClick={() => setSelectedSubPage('subscriptions')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/paid-courses')}
icon={<BookOpen size={16} />}
label="Paid Courses"
isActive={selectedSubPage === 'paid-courses'}
onClick={() => setSelectedSubPage('paid-courses')}
/>
<TabLink
href={getUriWithOrg(params.orgslug, '/dash/payments/customers')}
icon={<Users size={16} />}
label="Customers"
isActive={selectedSubPage === 'customers'}
onClick={() => setSelectedSubPage('customers')}
/>
</div>
</div>
<div className="h-6"></div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
className="h-full overflow-y-auto"
>
{selectedSubPage === 'general' && <div>General</div>}
{selectedSubPage === 'configuration' && <PaymentsConfigurationPage />}
{selectedSubPage === 'subscriptions' && <div>Subscriptions</div>}
{selectedSubPage === 'paid-courses' && <div>Paid Courses</div>}
{selectedSubPage === 'customers' && <div>Customers</div>}
</motion.div>
</div>
)
}
const TabLink = ({ href, icon, label, isActive, onClick }: { href: string, icon: React.ReactNode, label: string, isActive: boolean, onClick: () => void }) => (
<Link href={href}>
<div
onClick={onClick}
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
isActive ? 'border-b-4' : 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
{icon}
<div>{label}</div>
</div>
</div>
</Link>
)
export default PaymentsPage

View file

@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { useOrg } from '@components/Contexts/OrgContext';
import { SiStripe } from '@icons-pack/react-simple-icons'
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { getPaymentConfigs, createPaymentConfig, updatePaymentConfig } from '@services/payments/payments';
import FormLayout, { ButtonBlack, Input, Textarea, FormField, FormLabelAndMessage, Flex } from '@components/StyledElements/Form/Form';
import { Check, Edit } from 'lucide-react';
import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr';
import Modal from '@components/StyledElements/Modal/Modal';
const PaymentsConfigurationPage: React.FC = () => {
const org = useOrg() as any;
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: paymentConfigs, error, isLoading } = useSWR(
() => (org && access_token ? [`/payments/${org.id}/config`, access_token] : null),
([url, token]) => getPaymentConfigs(org.id, token)
);
const stripeConfig = paymentConfigs?.find((config: any) => config.provider === 'stripe');
const [isModalOpen, setIsModalOpen] = useState(false);
const enableStripe = async () => {
try {
const newConfig = { provider: 'stripe', enabled: true };
const config = await createPaymentConfig(org.id, newConfig, access_token);
toast.success('Stripe enabled successfully');
mutate([`/payments/${org.id}/config`, access_token]);
} catch (error) {
console.error('Error enabling Stripe:', error);
toast.error('Failed to enable Stripe');
}
};
const editConfig = async () => {
setIsModalOpen(true);
};
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error loading payment configuration</div>;
}
return (
<div>
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4">
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3">
<h1 className="font-bold text-xl text-gray-800">Payments Configuration</h1>
<h2 className="text-gray-500 text-md">Manage your organization payments configuration</h2>
</div>
<div className="flex flex-col py-4 px-6 rounded-lg light-shadow">
{stripeConfig ? (
<div className="flex items-center justify-between bg-white ">
<div className="flex items-center space-x-3">
<Check className="text-green-500" size={24} />
<span className="text-lg font-semibold">Stripe is enabled</span>
</div>
<ButtonBlack onClick={editConfig} className="flex items-center space-x-2 bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition duration-300">
<Edit size={16} />
<span>Edit Configuration</span>
</ButtonBlack>
</div>
) : (
<ButtonBlack onClick={enableStripe} className="flex items-center space-x-2 bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition duration-300">
<SiStripe size={16} />
<span>Enable Stripe</span>
</ButtonBlack>
)}
</div>
</div>
{stripeConfig && (
<EditStripeConfigModal
orgId={org.id}
configId={stripeConfig.id}
accessToken={access_token}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
)}
</div>
);
};
interface EditStripeConfigModalProps {
orgId: number;
configId: string;
accessToken: string;
isOpen: boolean;
onClose: () => void;
}
const EditStripeConfigModal: React.FC<EditStripeConfigModalProps> = ({ orgId, configId, accessToken, isOpen, onClose }) => {
const [stripeKey, setStripeKey] = useState('');
const [stripeSecretKey, setStripeSecretKey] = useState('');
const [stripeWebhookSecret, setStripeWebhookSecret] = useState('');
const handleSubmit = async () => {
try {
const stripe_config = {
stripe_key: stripeKey,
stripe_secret_key: stripeSecretKey,
stripe_webhook_secret: stripeWebhookSecret,
};
const updatedConfig = {
provider_config: stripe_config,
};
await updatePaymentConfig(orgId, configId, updatedConfig, accessToken);
toast.success('Configuration updated successfully');
mutate([`/payments/${orgId}/config`, accessToken]);
onClose();
} catch (error) {
console.error('Error updating config:', error);
toast.error('Failed to update configuration');
}
};
return (
<Modal isDialogOpen={isOpen} dialogTitle="Edit Stripe Configuration" dialogDescription='Edit your stripe configuration' onOpenChange={onClose}
dialogContent={
<FormLayout onSubmit={handleSubmit}>
<FormField name="stripe-key">
<FormLabelAndMessage label="Stripe Key" />
<Input type="password" value={stripeKey} onChange={(e) => setStripeKey(e.target.value)} />
</FormField>
<FormField name="stripe-secret-key">
<FormLabelAndMessage label="Stripe Secret Key" />
<Input type="password" value={stripeSecretKey} onChange={(e) => setStripeSecretKey(e.target.value)} />
</FormField>
<FormField name="stripe-webhook-secret">
<FormLabelAndMessage label="Stripe Webhook Secret" />
<Input type="password" value={stripeWebhookSecret} onChange={(e) => setStripeWebhookSecret(e.target.value)} />
</FormField>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<ButtonBlack type="submit" className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition duration-300">
Save
</ButtonBlack>
</Flex>
</FormLayout>
}
/>
);
};
export default PaymentsConfigurationPage;

View file

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useOrg } from '@components/Contexts/OrgContext'; import { useOrg } from '@components/Contexts/OrgContext';
import { Backpack, Book, ChevronRight, School, User, Users } from 'lucide-react' import { Backpack, Book, ChevronRight, CreditCard, School, User, Users } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
type BreadCrumbsProps = { type BreadCrumbsProps = {
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' | 'assignments' type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' | 'assignments' | 'payments'
last_breadcrumb?: string last_breadcrumb?: string
} }
@ -65,6 +65,15 @@ function BreadCrumbs(props: BreadCrumbsProps) {
) : ( ) : (
'' ''
)} )}
{props.type == 'payments' ? (
<div className="flex space-x-2 items-center">
{' '}
<CreditCard className="text-gray" size={14}></CreditCard>
<Link href="/dash/payments">Payments</Link>
</div>
) : (
''
)}
<div className="flex items-center space-x-1 first-letter:uppercase"> <div className="flex items-center space-x-1 first-letter:uppercase">
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''} {props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
<div className="first-letter:uppercase"> <div className="first-letter:uppercase">

View file

@ -0,0 +1,195 @@
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import { signOut } from 'next-auth/react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { Backpack, BookCopy, CreditCard, Home, LogOut, School, Settings, Users } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import React, { useEffect } from 'react'
import UserAvatar from '../UserAvatar'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
function DashLeftMenu() {
const org = useOrg() as any
const session = useLHSession() as any
const [loading, setLoading] = React.useState(true)
function waitForEverythingToLoad() {
if (org && session) {
return true
}
return false
}
async function logOutUI() {
const res = await signOut({ redirect: true, callbackUrl: getUriWithoutOrg('/login?orgslug=' + org.slug) })
if (res) {
getUriWithOrg(org.slug, '/')
}
}
useEffect(() => {
if (waitForEverythingToLoad()) {
setLoading(false)
}
}, [loading])
return (
<div
style={{
background:
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)',
}}
className="flex flex-col w-[90px] bg-black text-white shadow-xl h-screen sticky top-0"
>
<div className="flex flex-col h-full">
<div className="flex h-20 mt-6">
<Link
className="flex flex-col items-center mx-auto space-y-3"
href={'/'}
>
<ToolTip
content={'Back to Home'}
slateBlack
sideOffset={8}
side="right"
>
<Image
alt="Learnhouse logo"
width={40}
src={LearnHouseDashboardLogo}
/>
</ToolTip>
<ToolTip
content={'Your Organization'}
slateBlack
sideOffset={8}
side="right"
>
<div className="py-1 px-3 bg-black/40 opacity-40 rounded-md text-[10px] justify-center text-center">
{org?.name}
</div>
</ToolTip>
</Link>
</div>
<div className="flex grow flex-col justify-center space-y-5 items-center mx-auto">
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
</ToolTip> */}
<AdminAuthorization authorizationMode="component">
<ToolTip content={'Home'} slateBlack sideOffset={8} side="right">
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash`}
>
<Home size={18} />
</Link>
</ToolTip>
<ToolTip content={'Courses'} slateBlack sideOffset={8} side="right">
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/courses`}
>
<BookCopy size={18} />
</Link>
</ToolTip>
<ToolTip content={'Assignments'} slateBlack sideOffset={8} side="right">
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/assignments`}
>
<Backpack size={18} />
</Link>
</ToolTip>
<ToolTip content={'Users'} slateBlack sideOffset={8} side="right">
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/users/settings/users`}
>
<Users size={18} />
</Link>
</ToolTip>
<ToolTip
content={'Payments'}
slateBlack
sideOffset={8}
side="right"
>
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/payments/general`}
>
<CreditCard size={18} />
</Link>
</ToolTip>
<ToolTip
content={'Organization'}
slateBlack
sideOffset={8}
side="right"
>
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/org/settings/general`}
>
<School size={18} />
</Link>
</ToolTip>
</AdminAuthorization>
</div>
<div className="flex flex-col mx-auto pb-7 space-y-2">
<div className="flex items-center flex-col space-y-2">
<ToolTip
content={'@' + session.data.user.username}
slateBlack
sideOffset={8}
side="right"
>
<div className="mx-auto">
<UserAvatar border="border-4" width={35} />
</div>
</ToolTip>
<div className="flex items-center flex-col space-y-1">
<ToolTip
content={session.data.user.username + "'s Settings"}
slateBlack
sideOffset={8}
side="right"
>
<Link
href={'/dash/user-account/settings/general'}
className="py-3"
>
<Settings
className="mx-auto text-neutral-400 cursor-pointer"
size={18}
/>
</Link>
</ToolTip>
<ToolTip
content={'Logout'}
slateBlack
sideOffset={8}
side="right"
>
<LogOut
onClick={() => logOutUI()}
className="mx-auto text-neutral-400 cursor-pointer"
size={14}
/>
</ToolTip>
</div>
</div>
</div>
</div>
</div>
)
}
export default DashLeftMenu

View file

@ -3,12 +3,12 @@ import React from 'react'
import Link from 'next/link' import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config' import { getUriWithOrg } from '@services/config/config'
import { HeaderProfileBox } from '@components/Security/HeaderProfileBox' import { HeaderProfileBox } from '@components/Security/HeaderProfileBox'
import MenuLinks from './MenuLinks' import MenuLinks from './OrgMenuLinks'
import { getOrgLogoMediaDirectory } from '@services/media/media' import { getOrgLogoMediaDirectory } from '@services/media/media'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
export const Menu = (props: any) => { export const OrgMenu = (props: any) => {
const orgslug = props.orgslug const orgslug = props.orgslug
const session = useLHSession() as any; const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;

View file

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@hocuspocus/provider": "^2.13.6", "@hocuspocus/provider": "^2.13.6",
"@icons-pack/react-simple-icons": "^10.0.0",
"@radix-ui/colors": "^0.1.9", "@radix-ui/colors": "^0.1.9",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",

View file

@ -8425,8 +8425,8 @@ snapshots:
'@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4) '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1)
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
eslint-plugin-react: 7.36.1(eslint@8.57.1) eslint-plugin-react: 7.36.1(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
@ -8451,13 +8451,13 @@ snapshots:
debug: 4.3.7 debug: 4.3.7
enhanced-resolve: 5.17.1 enhanced-resolve: 5.17.1
eslint: 8.57.1 eslint: 8.57.1
eslint-module-utils: 2.11.1(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-module-utils: 2.11.1(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1)
fast-glob: 3.3.2 fast-glob: 3.3.2
get-tsconfig: 4.8.1 get-tsconfig: 4.8.1
is-bun-module: 1.2.1 is-bun-module: 1.2.1
is-glob: 4.0.3 is-glob: 4.0.3
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- "@typescript-eslint/parser" - "@typescript-eslint/parser"
- eslint-import-resolver-node - eslint-import-resolver-node
@ -8471,7 +8471,7 @@ snapshots:
'@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4) '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8486,7 +8486,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.11.1(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-module-utils: 2.11.1(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.15.1 is-core-module: 2.15.1
is-glob: 4.0.3 is-glob: 4.0.3

View file

@ -0,0 +1,38 @@
import { getAPIUrl } from '@services/config/config';
import { RequestBodyWithAuthHeader, errorHandling } from '@services/utils/ts/requests';
export async function getPaymentConfigs(orgId: number, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/config`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
);
const res = await errorHandling(result);
return res;
}
export async function createPaymentConfig(orgId: number, data: any, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/config`,
RequestBodyWithAuthHeader('POST', data, null, access_token)
);
const res = await errorHandling(result);
return res;
}
export async function updatePaymentConfig(orgId: number, id: string, data: any, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/config`,
RequestBodyWithAuthHeader('PUT', data, null, access_token)
);
const res = await errorHandling(result);
return res;
}
export async function deletePaymentConfig(orgId: number, id: string, access_token: string) {
const result = await fetch(
`${getAPIUrl()}payments/${orgId}/config/${id}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
);
const res = await errorHandling(result);
return res;
}