Skip to content

Commit

Permalink
feat(console): Seat removal (#2687)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cosmin-Parvulescu committed Sep 27, 2023
1 parent 70d8ca7 commit be30edb
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 12 deletions.
213 changes: 213 additions & 0 deletions apps/console/app/components/Billing/seating.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Listbox, Transition } from '@headlessui/react'
import { Button } from '@proofzero/design-system'
import { Text } from '@proofzero/design-system/src/atoms/text/Text'
import { ToastWithLink } from '@proofzero/design-system/src/atoms/toast/ToastWithLink'
Expand All @@ -9,6 +10,9 @@ import classnames from 'classnames'
import { useState } from 'react'
import { FaTrash } from 'react-icons/fa'
import {
HiArrowNarrowRight,
HiChevronDown,
HiChevronUp,
HiMinus,
HiOutlineShoppingCart,
HiOutlineX,
Expand Down Expand Up @@ -183,20 +187,219 @@ export const PurchaseGroupSeatingModal = ({
)
}

export const RemoveGroupSeatingModal = ({
isOpen,
setIsOpen,
removalFn,
seatsUsed,
totalSeats,
paymentIsSetup,
}: {
isOpen: boolean
setIsOpen: (open: boolean) => void
removalFn: (quantity: number) => void
seatsUsed: number
totalSeats: number
paymentIsSetup: boolean
}) => {
const [seatsNew, setSeatsNew] = useState(seatsUsed)
return (
<Modal isOpen={isOpen} handleClose={() => setIsOpen(false)}>
<div className="max-sm:w-screen sm:min-w-[640px] lg:min-w-[764px] w-fit">
<div className="pb-2 pt-5 px-5 w-full flex flex-row items-center justify-between">
<Text size="lg" weight="semibold" className="text-left text-gray-800">
Remove Additional User Seat(s)
</Text>
<div
className={`bg-white p-2 rounded-lg text-xl cursor-pointer
hover:bg-[#F3F4F6]`}
onClick={() => {
setIsOpen(false)
}}
>
<HiOutlineX />
</div>
</div>
<section className="p-5 pt-auto w-full">
<div className="w-full border rounded-lg overflow-auto thin-scrollbar">
<div className="p-6">
<Text
size="lg"
weight="semibold"
className="text-gray-900 text-left"
>
Additional User Seats
</Text>
<ul className="pl-4">
<li className="list-disc text-sm font-medium text-gray-500 text-left">
You are currently using {seatsUsed}/{totalSeats} Additional
User Seats
</li>
<li className="list-disc text-sm font-medium text-gray-500 text-left">
You can remove some Members of your Group if you'd like to pay
for fewer Seats.
</li>
</ul>
</div>
<div className="border-b border-gray-200"></div>
<div className="p-6 flex justify-between items-center">
<div>
<Text
size="sm"
weight="medium"
className="text-gray-800 text-left"
>
Number of Additional Seats
</Text>
<Text
size="sm"
weight="medium"
className="text-gray-500 text-left"
>{`${totalSeats} x ${seatingCost}/month`}</Text>
</div>

<div className="flex flex-row text-gray-500 space-x-4">
<div className="flex flex-row items-center space-x-2">
<Text size="sm">{totalSeats} Seat(s)</Text>
<HiArrowNarrowRight />
</div>

<div className="flex flex-row">
<Listbox
value={seatsNew}
onChange={setSeatsNew}
disabled={seatsUsed === totalSeats}
as="div"
>
{({ open }) => {
return (
<div>
<Listbox.Button
className="relative w-full cursor-default border
py-1.5 px-4 text-left sm:text-sm rounded-lg
focus:border-indigo-500 focus:outline-none focus:ring-1
flex flex-row space-x-3 items-center"
>
<Text size="sm">{seatsNew}</Text>
{open ? (
<HiChevronUp className="text-right" />
) : (
<HiChevronDown className="text-right" />
)}
</Listbox.Button>
<Transition
show={open}
as="div"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="bg-gray-800"
>
<Listbox.Options
className="absolute no-scrollbar w-full bg-white
rounded-lg border max-h-[150px] max-w-[66.1833px] overflow-auto"
>
{Array.apply(null, Array(totalSeats + 1)).map(
(_, i) => {
return i >= seatsUsed ? (
<Listbox.Option
key={i}
value={i}
className="flex items-center
cursor-pointer hover:bg-gray-100
rounded-lg m-1"
>
{({ selected }) => {
return (
<div
className={`w-full h-full px-4 py-1.5
rounded-lg ${
selected ? 'bg-gray-100 font-medium' : ''
}`}
>
{i}
</div>
)
}}
</Listbox.Option>
) : null
}
)}
</Listbox.Options>
</Transition>
</div>
)
}}
</Listbox>
</div>
</div>
</div>
<div className="border-b border-gray-200"></div>

<div className="p-6 flex justify-between items-center">
<Text
size="sm"
weight="medium"
className="text-gray-800 text-left"
>
Changes to your subscription
</Text>

<div className="flex flex-row gap-2 items-center">
<Text size="lg" weight="semibold" className="text-gray-900">{`${
seatingCost * (totalSeats - seatsNew) !== 0 ? '-' : ''
}$${seatingCost * (totalSeats - seatsNew)}`}</Text>
<Text size="sm" weight="medium" className="text-gray-500">
per month
</Text>
</div>
</div>
</div>
</section>
<section className="flex flex-row-reverse gap-4 mt-auto m-5">
<Button
btnType="dangerous-alt"
disabled={
!paymentIsSetup ||
seatsUsed === totalSeats ||
seatsNew < seatsUsed ||
seatsNew === totalSeats
}
onClick={() => {
setIsOpen(false)
setSeatsNew(seatsNew)

removalFn(seatsNew)
}}
>
Remove Seat(s)
</Button>
<Button btnType="secondary-alt" onClick={() => setIsOpen(false)}>
Cancel
</Button>
</section>
</div>
</Modal>
)
}

export const GroupSeatingCard = ({
groupID,
seatsTotal,
seatsUsed,
paymentData,
purchaseFn,
removalFn,
}: {
groupID: string
seatsTotal: number
seatsUsed: number
paymentData?: PaymentData
purchaseFn: (quantity: number) => void
removalFn: (quantity: number) => void
}) => {
const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false)
const [isRemovalModalOpen, setIsRemovalModalOpen] = useState(false)

return (
<>
Expand All @@ -208,6 +411,15 @@ export const GroupSeatingCard = ({
purchaseFn={purchaseFn}
/>

<RemoveGroupSeatingModal
isOpen={isRemovalModalOpen}
setIsOpen={setIsRemovalModalOpen}
removalFn={removalFn}
seatsUsed={seatsUsed}
totalSeats={seatsTotal}
paymentIsSetup={Boolean(paymentData?.paymentMethodID)}
/>

<article className="bg-white rounded-lg border">
<header className="flex flex-col lg:flex-row justify-between lg:items-center p-4 relative">
<div>
Expand Down Expand Up @@ -308,6 +520,7 @@ export const GroupSeatingCard = ({
<button
type="button"
className="flex flex-row items-center gap-3.5 text-indigo-500 cursor-pointer rounded-b-lg disabled:text-indigo-300"
onClick={() => setIsRemovalModalOpen(true)}
>
<FaTrash className="w-3.5 h-3.5" />
<Text size="sm" weight="medium">
Expand Down
17 changes: 16 additions & 1 deletion apps/console/app/routes/__layout/billing/groups/$groupID.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,22 @@ export default () => {
quantity: groupSeats.total + quantity,
customerID: paymentData?.customerID,
txType: 'buy',
txTarget: TxProduct.Seats,
txProduct: TxProduct.Seats,
}),
},
{
method: 'post',
}
)
}}
removalFn={(quantity) => {
submit(
{
payload: JSON.stringify({
quantity: quantity,
customerID: paymentData?.customerID,
txType: 'remove',
txProduct: TxProduct.Seats,
}),
},
{
Expand Down
12 changes: 6 additions & 6 deletions apps/console/app/routes/__layout/billing/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,13 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
})

const fd = await request.formData()
const { customerID, quantity, txType, txTarget } = JSON.parse(
const { customerID, quantity, txType, txProduct } = JSON.parse(
fd.get('payload') as string
) as {
customerID: string
quantity: number
txType: 'buy' | 'remove'
txTarget?: TxProduct
txProduct?: TxProduct
}

if ((quantity < 1 && txType === 'buy') || quantity < 0) {
Expand All @@ -268,7 +268,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
}

let sub
if (!txTarget || txTarget === TxProduct.Entitlements) {
if (!txProduct || txProduct === TxProduct.Entitlements) {
if (IdentityURNSpace.is(targetURN)) {
const apps = await coreClient.starbase.listApps.query()
const assignedEntitlementCount = apps.filter(
Expand All @@ -295,7 +295,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
subscriptionID: entitlements.subscriptionID,
URN: targetURN,
})
} else if (txTarget === TxProduct.Seats) {
} else if (txProduct === TxProduct.Seats) {
const seats = await coreClient.billing.getIdentityGroupSeats.query({
URN: groupURN as IdentityGroupURN,
})
Expand Down Expand Up @@ -338,7 +338,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
setPurchaseToastNotification({
sub,
flashSession,
txTarget,
txProduct,
})
}
if (txType === 'remove') {
Expand All @@ -347,7 +347,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
JSON.stringify({
type: ToastType.Success,
message: `${
!txTarget || txTarget === TxProduct.Entitlements
!txProduct || txProduct === TxProduct.Entitlements
? 'Entitlement(s)'
: 'Seat(s)'
} successfully removed`,
Expand Down
2 changes: 1 addition & 1 deletion apps/console/app/routes/__layout/groups/$groupID/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export default () => {
quantity: groupSeats.total + quantity,
customerID: paymentData?.customerID,
txType: 'buy',
txTarget: TxProduct.Seats,
txProduct: TxProduct.Seats,
}),
},
{
Expand Down
2 changes: 1 addition & 1 deletion apps/console/app/services/billing/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export const reconcileAppSubscriptions = async (
priceID: si.price.id,
quantity: si.quantity,
}))
.filter((pq) => Boolean(pq.quantity))
.filter((pq) => pq.quantity != null)

const priceIdToPlanTypeDict = {
[env.SECRET_STRIPE_PRO_PLAN_ID]: ServicePlanType.PRO,
Expand Down
6 changes: 3 additions & 3 deletions apps/console/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ export function useMatchesData(
export const setPurchaseToastNotification = ({
sub,
flashSession,
txTarget,
txProduct,
}: {
sub: Stripe.Subscription
flashSession: any
txTarget?: TxProduct
txProduct?: TxProduct
}) => {
const status = (sub.latest_invoice as Stripe.Invoice).status

Expand All @@ -77,7 +77,7 @@ export const setPurchaseToastNotification = ({
JSON.stringify({
type: ToastType.Success,
message: `${
!txTarget || txTarget === TxProduct.Entitlements
!txProduct || txProduct === TxProduct.Entitlements
? 'Entitlement(s)'
: 'Seat(s)'
} successfully bought`,
Expand Down

0 comments on commit be30edb

Please sign in to comment.