add badge
This commit is contained in:
@@ -1,9 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
|
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
|
||||||
import { CheckIcon, ChevronsUpDownIcon, PlusIcon } from 'lucide-react'
|
import {
|
||||||
import { createContext, useContext, useState } from 'react'
|
BadgeCheckIcon,
|
||||||
import { useLocation, useParams } from 'react-router'
|
BadgeIcon,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronsUpDownIcon,
|
||||||
|
PlusIcon
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { createContext, use } from 'react'
|
||||||
|
import { useLocation } from 'react-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -23,22 +29,22 @@ import {
|
|||||||
import { initials } from '@repo/ui/lib/utils'
|
import { initials } from '@repo/ui/lib/utils'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
export type Workspace = {
|
import type { Workspace, WorkspaceContextProps } from '@/middleware/workspace'
|
||||||
id: string
|
|
||||||
name: string
|
type Subscription = {
|
||||||
cnpj: string
|
billing_day: number
|
||||||
|
payment_method: 'PIX' | 'BANK_SLIP' | 'MANUAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceContextProps = {
|
const WorkspaceContext = createContext<
|
||||||
workspaces: Workspace[]
|
| (WorkspaceContextProps & {
|
||||||
activeWorkspace: Workspace
|
subscription: Subscription | null
|
||||||
setActiveWorkspace: React.Dispatch<React.SetStateAction<Workspace | null>>
|
})
|
||||||
}
|
| null
|
||||||
|
>(null)
|
||||||
const WorkspaceContext = createContext<WorkspaceContextProps | null>(null)
|
|
||||||
|
|
||||||
export function useWorksapce() {
|
export function useWorksapce() {
|
||||||
const ctx = useContext(WorkspaceContext)
|
const ctx = use(WorkspaceContext)
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error('WorkspaceContext is null')
|
throw new Error('WorkspaceContext is null')
|
||||||
@@ -48,20 +54,26 @@ export function useWorksapce() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceProvider({
|
export function WorkspaceProvider({
|
||||||
|
activeWorkspace,
|
||||||
workspaces,
|
workspaces,
|
||||||
|
subscription,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
|
activeWorkspace: Workspace
|
||||||
workspaces: Workspace[]
|
workspaces: Workspace[]
|
||||||
|
subscription?: Subscription
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { orgid } = useParams()
|
|
||||||
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | any>(
|
|
||||||
() => workspaces.find(({ id }) => id === orgid) || {}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContext
|
<WorkspaceContext
|
||||||
value={{ workspaces, activeWorkspace, setActiveWorkspace }}
|
value={{
|
||||||
|
activeWorkspace,
|
||||||
|
workspaces,
|
||||||
|
subscription:
|
||||||
|
subscription && Object.keys(subscription).length > 0
|
||||||
|
? subscription
|
||||||
|
: null
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WorkspaceContext>
|
</WorkspaceContext>
|
||||||
@@ -71,11 +83,10 @@ export function WorkspaceProvider({
|
|||||||
export function WorkspaceSwitcher() {
|
export function WorkspaceSwitcher() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { isMobile, state } = useSidebar()
|
const { isMobile, state } = useSidebar()
|
||||||
const { activeWorkspace, setActiveWorkspace, workspaces } = useWorksapce()
|
const { activeWorkspace, workspaces, subscription } = useWorksapce()
|
||||||
const [, fragment, _] = location.pathname.slice(1).split('/')
|
const [, fragment, _] = location.pathname.slice(1).split('/')
|
||||||
|
|
||||||
const onSelect = (workspace: Workspace) => {
|
const onSelect = (workspace: Workspace) => {
|
||||||
setActiveWorkspace(workspace)
|
|
||||||
window.location.assign(`/${workspace.id}/${fragment}`)
|
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"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground bg-secondary hover:bg-secondary/80 border cursor-pointer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="aria-expanded:border flex aspect-square size-8 items-center justify-center rounded-lg"
|
className="aria-expanded:border flex aspect-square size-8 items-center justify-center rounded-lg relative"
|
||||||
aria-expanded={state === 'expanded'}
|
aria-expanded={state === 'expanded'}
|
||||||
>
|
>
|
||||||
{initials(activeWorkspace?.name)}
|
{subscription && (
|
||||||
|
<BadgeCheckIcon className="fill-blue-500 absolute size-3 -top-1 -right-1" />
|
||||||
|
)}
|
||||||
|
{initials(activeWorkspace.name)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">
|
<span className="truncate font-medium">
|
||||||
{activeWorkspace?.name}
|
{activeWorkspace.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
{formatCNPJ(activeWorkspace?.cnpj)}
|
{formatCNPJ(activeWorkspace.cnpj)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDownIcon className="ml-auto" />
|
<ChevronsUpDownIcon className="ml-auto" />
|
||||||
|
|||||||
1
apps/admin.saladeaula.digital/app/conf.ts
Normal file
1
apps/admin.saladeaula.digital/app/conf.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const TZ = 'America/Sao_Paulo'
|
||||||
49
apps/admin.saladeaula.digital/app/middleware/workspace.ts
Normal file
49
apps/admin.saladeaula.digital/app/middleware/workspace.ts
Normal file
@@ -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<WorkspaceContextProps>()
|
||||||
|
|
||||||
|
export const workspaceMiddleware = async (
|
||||||
|
{ params, request, context }: LoaderFunctionArgs,
|
||||||
|
next: () => Promise<Response>
|
||||||
|
): Promise<Response> => {
|
||||||
|
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()
|
||||||
|
}
|
||||||
@@ -19,5 +19,3 @@ export const labels: Record<string, string> = {
|
|||||||
PENDING: 'Em aberto',
|
PENDING: 'Em aberto',
|
||||||
CLOSED: 'Fechado'
|
CLOSED: 'Fechado'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tz = 'America/Sao_Paulo'
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { ChevronRightIcon, ChevronLeftIcon } from 'lucide-react'
|
|||||||
import { subMonths, addMonths } from 'date-fns'
|
import { subMonths, addMonths } from 'date-fns'
|
||||||
import { DateTime as LuxonDateTime } from 'luxon'
|
import { DateTime as LuxonDateTime } from 'luxon'
|
||||||
|
|
||||||
|
import { TZ } from '@/conf'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { ButtonGroup } from '@repo/ui/components/ui/button-group'
|
import { ButtonGroup } from '@repo/ui/components/ui/button-group'
|
||||||
import { Badge } from '@repo/ui/components/ui/badge'
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
import { DateTime } from '@repo/ui/components/datetime'
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
|
|
||||||
import { formatDate, billingPeriod } from './util'
|
import { formatDate, billingPeriod } from './util'
|
||||||
import { tz } from './data'
|
|
||||||
|
|
||||||
type RangePeriodProps = {
|
type RangePeriodProps = {
|
||||||
startDate: Date
|
startDate: Date
|
||||||
@@ -28,7 +28,7 @@ export function RangePeriod({
|
|||||||
endDate,
|
endDate,
|
||||||
billingDay
|
billingDay
|
||||||
}: RangePeriodProps) {
|
}: RangePeriodProps) {
|
||||||
const today = LuxonDateTime.now().setZone(tz).toJSDate()
|
const today = LuxonDateTime.now().setZone(TZ).toJSDate()
|
||||||
const [, setSearchParams] = useSearchParams()
|
const [, setSearchParams] = useSearchParams()
|
||||||
const prevPeriod = billingPeriod(billingDay, subMonths(startDate, 1))
|
const prevPeriod = billingPeriod(billingDay, subMonths(startDate, 1))
|
||||||
const nextPeriod = billingPeriod(billingDay, addMonths(startDate, 1))
|
const nextPeriod = billingPeriod(billingDay, addMonths(startDate, 1))
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ import { Kbd } from '@repo/ui/components/ui/kbd'
|
|||||||
import { Currency } from '@repo/ui/components/currency'
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
import { DateTime } from '@repo/ui/components/datetime'
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
|
|
||||||
|
import { TZ } from '@/conf'
|
||||||
import { billingPeriod, formatDate } from './util'
|
import { billingPeriod, formatDate } from './util'
|
||||||
import { RangePeriod } from './range-period'
|
import { RangePeriod } from './range-period'
|
||||||
import { tz, statuses } from './data'
|
import { statuses } from './data'
|
||||||
|
|
||||||
export function meta({}) {
|
export function meta({}) {
|
||||||
return [{ title: 'Resumo de cobranças' }]
|
return [{ title: 'Resumo de cobranças' }]
|
||||||
@@ -55,7 +56,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const [startDate, endDate] = billingPeriod(
|
const [startDate, endDate] = billingPeriod(
|
||||||
billing_day,
|
billing_day,
|
||||||
LuxonDateTime.now().setZone(tz).toJSDate()
|
LuxonDateTime.now().setZone(TZ).toJSDate()
|
||||||
)
|
)
|
||||||
const start = searchParams.get('start') || formatDate(startDate)
|
const start = searchParams.get('start') || formatDate(startDate)
|
||||||
const end = searchParams.get('end') || formatDate(endDate)
|
const end = searchParams.get('end') || formatDate(endDate)
|
||||||
@@ -69,8 +70,8 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
|||||||
return {
|
return {
|
||||||
billing_day,
|
billing_day,
|
||||||
billing,
|
billing,
|
||||||
startDate: LuxonDateTime.fromISO(start, { zone: tz }).toJSDate(),
|
startDate: LuxonDateTime.fromISO(start, { zone: TZ }).toJSDate(),
|
||||||
endDate: LuxonDateTime.fromISO(end, { zone: tz }).toJSDate()
|
endDate: LuxonDateTime.fromISO(end, { zone: TZ }).toJSDate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { cloudflareContext } from '@repo/auth/context'
|
|||||||
import { headers, sortings, statuses } from '@repo/ui/routes/enrollments/data'
|
import { headers, sortings, statuses } from '@repo/ui/routes/enrollments/data'
|
||||||
|
|
||||||
import { columns, type Enrollment } from './columns'
|
import { columns, type Enrollment } from './columns'
|
||||||
|
import { useWorksapce } from '@/components/workspace-switcher'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Matrículas' }]
|
return [{ title: 'Matrículas' }]
|
||||||
@@ -66,6 +67,7 @@ export default function Route({
|
|||||||
loaderData: { enrollments }
|
loaderData: { enrollments }
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const { orgid } = useParams()
|
const { orgid } = useParams()
|
||||||
|
const { subscription } = useWorksapce()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
|
const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
|
||||||
const status = searchParams.get('status')
|
const status = searchParams.get('status')
|
||||||
@@ -203,7 +205,7 @@ export default function Route({
|
|||||||
<DataTableViewOptions className="flex-1" />
|
<DataTableViewOptions className="flex-1" />
|
||||||
|
|
||||||
<Button className="flex-1" asChild>
|
<Button className="flex-1" asChild>
|
||||||
<Link to="add">
|
<Link to={subscription ? 'add' : 'buy'}>
|
||||||
<PlusIcon /> Adicionar
|
<PlusIcon /> Adicionar
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
HoverCardTrigger
|
HoverCardTrigger
|
||||||
} from '@repo/ui/components/ui/hover-card'
|
} from '@repo/ui/components/ui/hover-card'
|
||||||
|
|
||||||
|
import { TZ } from '@/conf'
|
||||||
import {
|
import {
|
||||||
MAX_ITEMS,
|
MAX_ITEMS,
|
||||||
formSchema,
|
formSchema,
|
||||||
@@ -67,7 +68,7 @@ export function Assigned({ courses }: AssignedProps) {
|
|||||||
...e,
|
...e,
|
||||||
scheduled_for: e.scheduled_for
|
scheduled_for: e.scheduled_for
|
||||||
? DateTime.fromISO(e.scheduled_for, {
|
? DateTime.fromISO(e.scheduled_for, {
|
||||||
zone: 'America/Sao_Paulo'
|
zone: TZ
|
||||||
}).toJSDate()
|
}).toJSDate()
|
||||||
: undefined
|
: undefined
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,58 +2,58 @@ import type { Route } from './+types/route'
|
|||||||
|
|
||||||
import * as cookie from 'cookie'
|
import * as cookie from 'cookie'
|
||||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
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 {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger
|
SidebarTrigger
|
||||||
} from '@repo/ui/components/ui/sidebar'
|
} 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 { AppSidebar } from '@/components/app-sidebar'
|
||||||
|
import { workspaceMiddleware, workspaceContext } from '@/middleware/workspace'
|
||||||
|
|
||||||
// import { Notification } from '@/components/notification'
|
// 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) {
|
export async function loader({ params, context, request }: Route.ActionArgs) {
|
||||||
const user = context.get(userContext)!
|
const user = context.get(userContext)!
|
||||||
|
const { activeWorkspace, workspaces } = context.get(workspaceContext)
|
||||||
const rawCookie = request.headers.get('cookie') || ''
|
const rawCookie = request.headers.get('cookie') || ''
|
||||||
const parsedCookies = cookie.parse(rawCookie)
|
const parsedCookies = cookie.parse(rawCookie)
|
||||||
const { sidebar_state = 'true' } = parsedCookies
|
const { sidebar_state = 'true' } = parsedCookies
|
||||||
|
|
||||||
const r = await req({
|
const subscription = req({
|
||||||
url: `/users/${user.sub}/orgs?limit=25`,
|
url: `/orgs/${activeWorkspace.id}/subscription`,
|
||||||
request,
|
request,
|
||||||
context
|
context
|
||||||
})
|
}).then((r) => r.json())
|
||||||
|
|
||||||
if (!r.ok) {
|
const address = req({
|
||||||
throw new Response(await r.text(), { status: r.status })
|
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({
|
export function shouldRevalidate({
|
||||||
@@ -64,7 +64,14 @@ export function shouldRevalidate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({ loaderData }: Route.ComponentProps) {
|
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(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined' && window.rybbit) {
|
if (typeof window !== 'undefined' && window.rybbit) {
|
||||||
@@ -77,7 +84,11 @@ export default function Route({ loaderData }: Route.ComponentProps) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceProvider workspaces={orgs as Workspace[]}>
|
<WorkspaceProvider
|
||||||
|
activeWorkspace={activeWorkspace}
|
||||||
|
workspaces={workspaces}
|
||||||
|
subscription={subscription}
|
||||||
|
>
|
||||||
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">
|
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user