addGiftBudgetAction.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import {openModal} from 'sentry/actionCreators/modal';
  6. import InputField from 'sentry/components/forms/fields/inputField';
  7. import NumberField from 'sentry/components/forms/fields/numberField';
  8. import TextField from 'sentry/components/forms/fields/textField';
  9. import Form from 'sentry/components/forms/form';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Organization} from 'sentry/types/organization';
  13. import useApi from 'sentry/utils/useApi';
  14. import type {Subscription} from 'getsentry/types';
  15. import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
  16. type Props = {
  17. onSuccess: () => void;
  18. organization: Organization;
  19. subscription: Subscription;
  20. };
  21. type ModalProps = Props & ModalRenderProps;
  22. function AddGiftBudgetModal({
  23. onSuccess,
  24. organization,
  25. subscription,
  26. closeModal,
  27. Header,
  28. Body,
  29. }: ModalProps) {
  30. const api = useApi();
  31. const [selectedBudgetId, setSelectedBudgetId] = useState<string | null>(null);
  32. const [giftAmount, setGiftAmount] = useState<number>(0);
  33. const [ticketUrl, setTicketUrl] = useState<string | null>(null);
  34. const [notes, setNotes] = useState<string | null>(null);
  35. const reservedBudgetOptions = useMemo(
  36. () => subscription.reservedBudgets ?? [],
  37. [subscription.reservedBudgets]
  38. );
  39. useEffect(() => {
  40. if (reservedBudgetOptions.length > 0 && !selectedBudgetId) {
  41. setSelectedBudgetId(reservedBudgetOptions[0]?.id ?? null);
  42. }
  43. }, [reservedBudgetOptions, selectedBudgetId]);
  44. const onSubmit = () => {
  45. if (!selectedBudgetId || giftAmount <= 0) {
  46. return;
  47. }
  48. const selectedBudget = reservedBudgetOptions.find(
  49. budget => budget.id === selectedBudgetId
  50. );
  51. const data = {
  52. freeReservedBudget: {
  53. id: selectedBudgetId,
  54. freeBudget: giftAmount * 100, // convert to cents
  55. categories: Object.keys(selectedBudget?.categories ?? []),
  56. },
  57. ticketUrl,
  58. notes,
  59. };
  60. api.request(`/customers/${organization.slug}/`, {
  61. method: 'PUT',
  62. data,
  63. success: () => {
  64. addSuccessMessage('Added gifted budget amount.');
  65. closeModal();
  66. onSuccess();
  67. },
  68. error: () => {
  69. addErrorMessage('Unable to add gifted budget amount for org.');
  70. },
  71. });
  72. };
  73. function getHelp() {
  74. return `Total Gift: $${giftAmount.toLocaleString()}`;
  75. }
  76. return (
  77. <Fragment>
  78. <Header closeButton>Add Gift Budget</Header>
  79. <Body>
  80. {reservedBudgetOptions.length > 1 ? (
  81. <Fragment>
  82. <div>Select a reserved budget to add gift amount.</div>
  83. <br />
  84. </Fragment>
  85. ) : reservedBudgetOptions.length === 0 ? (
  86. <div>No reserved budgets available.</div>
  87. ) : (
  88. <div />
  89. )}
  90. <Form onSubmit={onSubmit} submitLabel={t('Confirm')} onCancel={closeModal}>
  91. {reservedBudgetOptions.map(budget => (
  92. <BudgetCard
  93. key={budget.id}
  94. isSelected={selectedBudgetId === budget.id}
  95. onClick={() => setSelectedBudgetId(budget.id)}
  96. >
  97. <BudgetHeader>
  98. <div>
  99. <strong>Reserved Budget:</strong> $
  100. {(budget.reservedBudget / 100).toLocaleString()}
  101. </div>
  102. <div>
  103. <strong>Existing Free Budget:</strong> $
  104. {(budget.freeBudget / 100).toLocaleString()}
  105. </div>
  106. </BudgetHeader>
  107. <BudgetCategories>
  108. <strong>Categories:</strong>{' '}
  109. {Object.keys(budget.categories)
  110. .map(category =>
  111. getPlanCategoryName({
  112. plan: subscription.planDetails,
  113. category,
  114. capitalize: false,
  115. hadCustomDynamicSampling: true,
  116. })
  117. )
  118. .join(', ') || 'None'}
  119. </BudgetCategories>
  120. {selectedBudgetId === budget.id && (
  121. <NumberField
  122. inline={false}
  123. stacked
  124. flexibleControlStateSize
  125. label="Gift Amount ($)"
  126. help={
  127. <Fragment>
  128. <Fragment>Enter gift amount in dollars (max $10,000).</Fragment>
  129. <br />
  130. <Fragment>{getHelp()}</Fragment>
  131. </Fragment>
  132. }
  133. name="giftAmount"
  134. value={giftAmount}
  135. defaultValue={0}
  136. onChange={(value: number) => {
  137. const clampedValue = Math.min(10000, Math.max(0, value));
  138. setGiftAmount(clampedValue);
  139. }}
  140. required
  141. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  142. />
  143. )}
  144. </BudgetCard>
  145. ))}
  146. {reservedBudgetOptions.length === 0 && (
  147. <div>No reserved budgets available.</div>
  148. )}
  149. <AuditFields>
  150. <InputField
  151. data-test-id="url-field"
  152. name="ticket-url"
  153. type="url"
  154. label="TicketUrl"
  155. inline={false}
  156. stacked
  157. flexibleControlStateSize
  158. onChange={(ticketUrlInput: any) => setTicketUrl(ticketUrlInput)}
  159. />
  160. <TextField
  161. data-test-id="notes-field"
  162. name="notes"
  163. label="Notes"
  164. inline={false}
  165. stacked
  166. flexibleControlStateSize
  167. maxLength={500}
  168. required // serializer requires this to be present
  169. onChange={(notesInput: any) => setNotes(notesInput)}
  170. />
  171. </AuditFields>
  172. </Form>
  173. </Body>
  174. </Fragment>
  175. );
  176. }
  177. type Options = Pick<Props, 'onSuccess' | 'organization' | 'subscription'>;
  178. const addGiftBudgetAction = (opts: Options) => {
  179. return openModal(deps => <AddGiftBudgetModal {...deps} {...opts} />, {
  180. closeEvents: 'escape-key',
  181. });
  182. };
  183. export default addGiftBudgetAction;
  184. const BudgetCard = styled('div')<{isSelected: boolean}>`
  185. padding: ${space(2)};
  186. margin: ${space(1)} 0;
  187. border: 1px solid ${p => p.theme.border};
  188. border-radius: ${p => p.theme.borderRadius};
  189. background-color: ${p => (p.isSelected ? p.theme.surface100 : 'transparent')};
  190. cursor: pointer;
  191. `;
  192. const BudgetHeader = styled('div')`
  193. display: flex;
  194. justify-content: space-between;
  195. margin-bottom: ${space(1)};
  196. `;
  197. const BudgetCategories = styled('div')`
  198. margin-bottom: ${space(1)};
  199. `;
  200. const AuditFields = styled('div')`
  201. margin-top: ${space(2)};
  202. `;