diff --git a/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx b/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx index ef0555d..16a6527 100644 --- a/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx +++ b/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx @@ -1,9 +1,15 @@ 'use client' import { formatCNPJ } from '@brazilian-utils/brazilian-utils' -import { CheckIcon, ChevronsUpDownIcon, PlusIcon } from 'lucide-react' -import { createContext, useContext, useState } from 'react' -import { useLocation, useParams } from 'react-router' +import { + BadgeCheckIcon, + BadgeIcon, + CheckIcon, + ChevronsUpDownIcon, + PlusIcon +} from 'lucide-react' +import { createContext, use } from 'react' +import { useLocation } from 'react-router' import { DropdownMenu, @@ -23,22 +29,22 @@ import { import { initials } from '@repo/ui/lib/utils' import { Link } from 'react-router' -export type Workspace = { - id: string - name: string - cnpj: string +import type { Workspace, WorkspaceContextProps } from '@/middleware/workspace' + +type Subscription = { + billing_day: number + payment_method: 'PIX' | 'BANK_SLIP' | 'MANUAL' } -type WorkspaceContextProps = { - workspaces: Workspace[] - activeWorkspace: Workspace - setActiveWorkspace: React.Dispatch> -} - -const WorkspaceContext = createContext(null) +const WorkspaceContext = createContext< + | (WorkspaceContextProps & { + subscription: Subscription | null + }) + | null +>(null) export function useWorksapce() { - const ctx = useContext(WorkspaceContext) + const ctx = use(WorkspaceContext) if (!ctx) { throw new Error('WorkspaceContext is null') @@ -48,20 +54,26 @@ export function useWorksapce() { } export function WorkspaceProvider({ + activeWorkspace, workspaces, + subscription, children }: { + activeWorkspace: Workspace workspaces: Workspace[] + subscription?: Subscription children: React.ReactNode }) { - const { orgid } = useParams() - const [activeWorkspace, setActiveWorkspace] = useState( - () => workspaces.find(({ id }) => id === orgid) || {} - ) - return ( 0 + ? subscription + : null + }} > {children} @@ -71,11 +83,10 @@ export function WorkspaceProvider({ export function WorkspaceSwitcher() { const location = useLocation() const { isMobile, state } = useSidebar() - const { activeWorkspace, setActiveWorkspace, workspaces } = useWorksapce() + const { activeWorkspace, workspaces, subscription } = useWorksapce() const [, fragment, _] = location.pathname.slice(1).split('/') const onSelect = (workspace: Workspace) => { - setActiveWorkspace(workspace) window.location.assign(`/${workspace.id}/${fragment}`) } @@ -89,17 +100,20 @@ export function WorkspaceSwitcher() { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground bg-secondary hover:bg-secondary/80 border cursor-pointer" >
- {initials(activeWorkspace?.name)} + {subscription && ( + + )} + {initials(activeWorkspace.name)}
- {activeWorkspace?.name} + {activeWorkspace.name} - {formatCNPJ(activeWorkspace?.cnpj)} + {formatCNPJ(activeWorkspace.cnpj)}
diff --git a/apps/admin.saladeaula.digital/app/conf.ts b/apps/admin.saladeaula.digital/app/conf.ts new file mode 100644 index 0000000..2597a6d --- /dev/null +++ b/apps/admin.saladeaula.digital/app/conf.ts @@ -0,0 +1 @@ +export const TZ = 'America/Sao_Paulo' diff --git a/apps/admin.saladeaula.digital/app/middleware/workspace.ts b/apps/admin.saladeaula.digital/app/middleware/workspace.ts new file mode 100644 index 0000000..b4f22dc --- /dev/null +++ b/apps/admin.saladeaula.digital/app/middleware/workspace.ts @@ -0,0 +1,49 @@ +import { type LoaderFunctionArgs, createContext } from 'react-router' + +import { userContext } from '@repo/auth/context' +import { request as req } from '@repo/util/request' + +export type Workspace = { + id: string + name: string + cnpj: string +} + +export type WorkspaceContextProps = { + activeWorkspace: Workspace + workspaces: Workspace[] +} + +export const workspaceContext = createContext() + +export const workspaceMiddleware = async ( + { params, request, context }: LoaderFunctionArgs, + next: () => Promise +): Promise => { + const org_id = params.orgid + const user = context.get(userContext)! + + const r = await req({ + url: `/users/${user.sub}/orgs?limit=25`, + request, + context + }) + + if (!r.ok) { + throw new Response(await r.text(), { status: r.status }) + } + + const { items } = (await r.json()) as { items: { sk: string }[] } + const workspaces = items.map(({ sk, ...props }) => { + const [, id] = sk?.split('#') + return { ...props, id } + }) as Workspace[] + + const activeWorkspace = workspaces.find( + ({ id }) => id === org_id + ) as Workspace + + context.set(workspaceContext, { activeWorkspace, workspaces }) + + return await next() +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts index 59dc1e0..691cc9c 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts @@ -19,5 +19,3 @@ export const labels: Record = { PENDING: 'Em aberto', CLOSED: 'Fechado' } - -export const tz = 'America/Sao_Paulo' diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/range-period.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/range-period.tsx index 442ff0a..618e91a 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/range-period.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/range-period.tsx @@ -3,13 +3,13 @@ import { ChevronRightIcon, ChevronLeftIcon } from 'lucide-react' import { subMonths, addMonths } from 'date-fns' import { DateTime as LuxonDateTime } from 'luxon' +import { TZ } from '@/conf' import { Button } from '@repo/ui/components/ui/button' import { ButtonGroup } from '@repo/ui/components/ui/button-group' import { Badge } from '@repo/ui/components/ui/badge' import { DateTime } from '@repo/ui/components/datetime' import { formatDate, billingPeriod } from './util' -import { tz } from './data' type RangePeriodProps = { startDate: Date @@ -28,7 +28,7 @@ export function RangePeriod({ endDate, billingDay }: RangePeriodProps) { - const today = LuxonDateTime.now().setZone(tz).toJSDate() + const today = LuxonDateTime.now().setZone(TZ).toJSDate() const [, setSearchParams] = useSearchParams() const prevPeriod = billingPeriod(billingDay, subMonths(startDate, 1)) const nextPeriod = billingPeriod(billingDay, addMonths(startDate, 1)) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/route.tsx index f296d7b..43e7a52 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/route.tsx @@ -33,9 +33,10 @@ import { Kbd } from '@repo/ui/components/ui/kbd' import { Currency } from '@repo/ui/components/currency' import { DateTime } from '@repo/ui/components/datetime' +import { TZ } from '@/conf' import { billingPeriod, formatDate } from './util' import { RangePeriod } from './range-period' -import { tz, statuses } from './data' +import { statuses } from './data' export function meta({}) { return [{ title: 'Resumo de cobranças' }] @@ -55,7 +56,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) { const [startDate, endDate] = billingPeriod( billing_day, - LuxonDateTime.now().setZone(tz).toJSDate() + LuxonDateTime.now().setZone(TZ).toJSDate() ) const start = searchParams.get('start') || formatDate(startDate) const end = searchParams.get('end') || formatDate(endDate) @@ -69,8 +70,8 @@ export async function loader({ context, request, params }: Route.LoaderArgs) { return { billing_day, billing, - startDate: LuxonDateTime.fromISO(start, { zone: tz }).toJSDate(), - endDate: LuxonDateTime.fromISO(end, { zone: tz }).toJSDate() + startDate: LuxonDateTime.fromISO(start, { zone: TZ }).toJSDate(), + endDate: LuxonDateTime.fromISO(end, { zone: TZ }).toJSDate() } } diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx index 3c3c0de..422408f 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx @@ -19,6 +19,7 @@ import { cloudflareContext } from '@repo/auth/context' import { headers, sortings, statuses } from '@repo/ui/routes/enrollments/data' import { columns, type Enrollment } from './columns' +import { useWorksapce } from '@/components/workspace-switcher' export function meta({}: Route.MetaArgs) { return [{ title: 'Matrículas' }] @@ -66,6 +67,7 @@ export default function Route({ loaderData: { enrollments } }: Route.ComponentProps) { const { orgid } = useParams() + const { subscription } = useWorksapce() const [searchParams, setSearchParams] = useSearchParams() const [selectedRows, setSelectedRows] = useState([]) const status = searchParams.get('status') @@ -203,7 +205,7 @@ export default function Route({ diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx index 6b6f94e..cc150ea 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx @@ -28,6 +28,7 @@ import { HoverCardTrigger } from '@repo/ui/components/ui/hover-card' +import { TZ } from '@/conf' import { MAX_ITEMS, formSchema, @@ -67,7 +68,7 @@ export function Assigned({ courses }: AssignedProps) { ...e, scheduled_for: e.scheduled_for ? DateTime.fromISO(e.scheduled_for, { - zone: 'America/Sao_Paulo' + zone: TZ }).toJSDate() : undefined })) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx index 9fe6ad1..4bcf3e7 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx @@ -2,58 +2,58 @@ import type { Route } from './+types/route' import * as cookie from 'cookie' import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router' -import { useEffect } from 'react' +import { use, useEffect } from 'react' -import { request as req } from '@repo/util/request' -import { - WorkspaceProvider, - type Workspace -} from '@/components/workspace-switcher' -import { userContext } from '@repo/auth/context' -import { Toaster } from '@repo/ui/components/ui/sonner' -import { authMiddleware } from '@repo/auth/middleware/auth' -import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' -import { NavUser } from '@repo/ui/components/nav-user' import { SidebarInset, SidebarProvider, SidebarTrigger } from '@repo/ui/components/ui/sidebar' +import { userContext } from '@repo/auth/context' +import { request as req } from '@repo/util/request' +import { authMiddleware } from '@repo/auth/middleware/auth' +import { Toaster } from '@repo/ui/components/ui/sonner' +import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' +import { NavUser } from '@repo/ui/components/nav-user' +import { WorkspaceProvider } from '@/components/workspace-switcher' import { AppSidebar } from '@/components/app-sidebar' +import { workspaceMiddleware, workspaceContext } from '@/middleware/workspace' // import { Notification } from '@/components/notification' -export const middleware: Route.MiddlewareFunction[] = [authMiddleware] +export const middleware: Route.MiddlewareFunction[] = [ + authMiddleware, + workspaceMiddleware +] export async function loader({ params, context, request }: Route.ActionArgs) { const user = context.get(userContext)! + const { activeWorkspace, workspaces } = context.get(workspaceContext) const rawCookie = request.headers.get('cookie') || '' const parsedCookies = cookie.parse(rawCookie) const { sidebar_state = 'true' } = parsedCookies - const r = await req({ - url: `/users/${user.sub}/orgs?limit=25`, + const subscription = req({ + url: `/orgs/${activeWorkspace.id}/subscription`, request, context - }) + }).then((r) => r.json()) - if (!r.ok) { - throw new Response(await r.text(), { status: r.status }) + const address = req({ + url: `/orgs/${activeWorkspace.id}/address`, + request, + context + }).then((r) => r.json()) + + return { + user, + activeWorkspace, + workspaces, + sidebar_state, + subscription, + address } - - const { items = [] } = (await r.json()) as { items: { sk: string }[] } - const orgs = items.map(({ sk, ...props }) => { - const [, id] = sk?.split('#') - return { ...props, id } - }) - const exists = orgs.some(({ id }) => id === params.orgid) - - if (exists) { - return { user, orgs, sidebar_state } - } - - throw new Response(null, { status: 401 }) } export function shouldRevalidate({ @@ -64,7 +64,14 @@ export function shouldRevalidate({ } export default function Route({ loaderData }: Route.ComponentProps) { - const { user, orgs, sidebar_state } = loaderData + const { + user, + activeWorkspace, + workspaces, + sidebar_state, + subscription: subscription_ + } = loaderData + const subscription = use(subscription_) useEffect(() => { if (typeof window !== 'undefined' && window.rybbit) { @@ -77,7 +84,11 @@ export default function Route({ loaderData }: Route.ComponentProps) { }, []) return ( - +