@accounter/client 0.0.8-alpha-20251021150615-800574fc6d416cd319de216c97b431643d8958a2 → 0.0.8-alpha-20251021225827-178e480c997a9811913e16f85cb94329041b096e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/dist/assets/Checkbox-CxedbJAl.js +6 -0
  3. package/dist/assets/Progress-D5SuJtCd.js +1 -0
  4. package/dist/assets/Typography-BQFz-z7L.js +1 -0
  5. package/dist/assets/accordion-COWOBKuq.js +1 -0
  6. package/dist/assets/accountant-approvals-Bd2y8us_.js +1 -0
  7. package/dist/assets/all-charges-SWBnaZu7.js +1 -0
  8. package/dist/assets/arrow-up-down-dZmrBLse.js +6 -0
  9. package/dist/assets/business--GVVfDEa.js +37 -0
  10. package/dist/assets/business-transactions-single-BsbkUf_H.js +1 -0
  11. package/dist/assets/business-trip-ByXPVXdG.js +1 -0
  12. package/dist/assets/charges-filters-D43UbXob.js +1 -0
  13. package/dist/assets/charges-ledger-validation-D0uMH_JE.js +1 -0
  14. package/dist/assets/chart-ClU1KbWe.js +74 -0
  15. package/dist/assets/data-table-pagination-D9Y0_Tn8.js +11 -0
  16. package/dist/assets/editable-business-trip-DhqOQBPa.js +16 -0
  17. package/dist/assets/graphql-document-dedupe-fragments-ByT8-wlV.js +1 -0
  18. package/dist/assets/index-1U6rQgQe.js +6 -0
  19. package/dist/assets/index-3-AKn8tg.js +1 -0
  20. package/dist/assets/index-91A2PLZ6.js +137 -0
  21. package/dist/assets/index-BBHuCWRn.js +1 -0
  22. package/dist/assets/index-BPNuFFtx.js +1 -0
  23. package/dist/assets/index-BXqHnRVY.js +1 -0
  24. package/dist/assets/index-BciOH8FS.js +1 -0
  25. package/dist/assets/index-BjHuUHDO.js +1 -0
  26. package/dist/assets/index-BxKmoNQd.js +1 -0
  27. package/dist/assets/index-C3bqiFIv.js +2 -0
  28. package/dist/assets/index-C5MeepK_.js +11 -0
  29. package/dist/assets/index-CAwm68Mg.js +1 -0
  30. package/dist/assets/index-CJ8OGXxv.js +1 -0
  31. package/dist/assets/index-CJyY-qF6.js +1 -0
  32. package/dist/assets/index-CMYnx46_.js +6 -0
  33. package/dist/assets/index-CNrwxUZ7.js +1 -0
  34. package/dist/assets/index-CvV5z5r9.js +876 -0
  35. package/dist/assets/index-D08H2GXq.js +17 -0
  36. package/dist/assets/index-GFsPY1p4.js +2 -0
  37. package/dist/assets/index-KwNwThNu.js +1 -0
  38. package/dist/assets/index-YA8IBFyB.js +1 -0
  39. package/dist/assets/index-ZpyI3qxW.js +24 -0
  40. package/dist/assets/index-gdTXrWXt.css +1 -0
  41. package/dist/assets/index-ytnIEraq.js +9 -0
  42. package/dist/assets/{index.es-DHwHzag1.js → index.es-CYeQ4a5s.js} +5 -5
  43. package/dist/assets/issue-document-CdikNnO2.js +1 -0
  44. package/dist/assets/login-page-effgZS3V.js +1 -0
  45. package/dist/assets/missing-info-charges-CnPFTzoZ.js +1 -0
  46. package/dist/assets/page-not-found-D8YlgDOm.js +1 -0
  47. package/dist/assets/pencil-mxW0-tGM.js +6 -0
  48. package/dist/assets/report-commentary-row-DCozKgVE.js +1 -0
  49. package/dist/assets/save-CHlytUqu.js +11 -0
  50. package/dist/assets/sequential-CAnleQny.js +1 -0
  51. package/dist/assets/similar-charges-by-business-modal-Dzbspk_r.js +1 -0
  52. package/dist/assets/sub-Cp_PhKiD.js +1 -0
  53. package/dist/assets/subMonths-DCj_iXAn.js +1 -0
  54. package/dist/index.html +2 -2
  55. package/package.json +6 -5
  56. package/src/app.tsx +35 -25
  57. package/src/components/business/business-header.tsx +68 -0
  58. package/src/components/business/charges-section.tsx +82 -0
  59. package/src/components/business/charts-section.tsx +115 -0
  60. package/src/components/business/configurations-section.tsx +885 -0
  61. package/src/components/business/contact-info-section.tsx +536 -0
  62. package/src/components/business/contracts-section.tsx +196 -0
  63. package/src/components/business/documents-section.tsx +26 -0
  64. package/src/components/business/index.tsx +171 -0
  65. package/src/components/business/integrations-section.tsx +477 -0
  66. package/src/components/business/transactions-section.tsx +26 -0
  67. package/src/components/business-transactions/business-extended-info.tsx +22 -28
  68. package/src/components/business-transactions/business-transactions-single.tsx +3 -3
  69. package/src/components/business-transactions/index.tsx +13 -2
  70. package/src/components/business-trips/business-trip.tsx +3 -3
  71. package/src/components/charges/cells/business-trip.tsx +6 -8
  72. package/src/components/charges/cells/counterparty.tsx +7 -5
  73. package/src/components/charges/charge-extended-info-menu.tsx +27 -21
  74. package/src/components/charges/charges-row.tsx +12 -10
  75. package/src/components/charges/charges-table.tsx +15 -9
  76. package/src/components/clients/contracts/modify-contract-dialog.tsx +464 -0
  77. package/src/components/clients/modify-client-dialog.tsx +276 -0
  78. package/src/components/common/accounter-table.tsx +6 -5
  79. package/src/components/common/business-trip-report/parts/core-expense-row.tsx +11 -9
  80. package/src/components/common/business-trip-report/parts/uncategorized-transactions.tsx +11 -13
  81. package/src/components/common/buttons/index.ts +0 -2
  82. package/src/components/common/buttons/logout-button.tsx +7 -6
  83. package/src/components/common/documents/issue-document/index.tsx +3 -3
  84. package/src/components/common/documents/issue-document/{recent-client-docs.tsx → recent-business-docs.tsx} +19 -13
  85. package/src/components/common/documents-to-charge-matcher/selection-handler/index.tsx +4 -2
  86. package/src/components/common/documents-to-charge-matcher/selection-handler/wide-filtered-selection.tsx +5 -7
  87. package/src/components/common/forms/business-card.tsx +1 -0
  88. package/src/components/common/forms/edit-document.tsx +23 -10
  89. package/src/components/common/forms/modify-business-fields.tsx +2 -19
  90. package/src/components/common/inputs/combo-box.tsx +1 -1
  91. package/src/components/common/new-documents-list.tsx +10 -8
  92. package/src/components/documents-table/cells/creditor.tsx +11 -4
  93. package/src/components/documents-table/cells/debtor.tsx +11 -4
  94. package/src/components/error-boundary.tsx +189 -0
  95. package/src/components/layout/breadcrumbs.tsx +77 -0
  96. package/src/components/layout/dashboard-layout.tsx +4 -0
  97. package/src/components/layout/document-title.tsx +31 -0
  98. package/src/components/layout/navigation-progress.tsx +52 -0
  99. package/src/components/layout/page-skeleton.tsx +49 -0
  100. package/src/components/layout/sidelinks.tsx +28 -27
  101. package/src/components/ledger-table/counterparty-cell.tsx +19 -13
  102. package/src/components/login-page.tsx +2 -1
  103. package/src/components/reports/corporate-tax-ruling-compliance-report/index.tsx +3 -3
  104. package/src/components/reports/profit-and-loss-report/index.tsx +3 -3
  105. package/src/components/reports/tax-report/index.tsx +3 -3
  106. package/src/components/reports/trial-balance-report/trial-balance-report-group.tsx +4 -6
  107. package/src/components/reports/trial-balance-report/trial-balance-report-sort-code.tsx +8 -11
  108. package/src/components/screens/businesses/business.tsx +56 -0
  109. package/src/components/screens/charges/charge.tsx +22 -9
  110. package/src/components/screens/documents/issue-documents/edit-issue-document-modal.tsx +4 -4
  111. package/src/components/transactions-table/cells/counterparty.tsx +9 -2
  112. package/src/components/transactions-table/cells-legacy/counterparty.tsx +9 -2
  113. package/src/components/ui/progress.tsx +25 -0
  114. package/src/components/ui/skeleton.tsx +12 -0
  115. package/src/gql/gql.ts +93 -9
  116. package/src/gql/graphql.ts +289 -9
  117. package/src/helpers/contracts.ts +22 -0
  118. package/src/helpers/currency.ts +5 -0
  119. package/src/helpers/index.ts +2 -0
  120. package/src/helpers/pcn874.ts +17 -0
  121. package/src/hooks/use-add-sort-code.ts +1 -1
  122. package/src/hooks/use-add-tag.ts +1 -1
  123. package/src/hooks/use-create-contract.ts +62 -0
  124. package/src/hooks/use-delete-contract.ts +64 -0
  125. package/src/hooks/use-delete-tag.ts +1 -1
  126. package/src/hooks/use-get-all-contracts.ts +0 -1
  127. package/src/hooks/use-insert-client.ts +80 -0
  128. package/src/hooks/use-merge-businesses.ts +1 -1
  129. package/src/hooks/use-merge-charges.ts +1 -1
  130. package/src/hooks/use-update-client.ts +75 -0
  131. package/src/hooks/use-update-contract.ts +69 -0
  132. package/src/index.tsx +4 -22
  133. package/src/providers/auth-guard.tsx +14 -23
  134. package/src/providers/index.tsx +7 -2
  135. package/src/providers/urql-client.ts +86 -0
  136. package/src/providers/urql.tsx +7 -12
  137. package/src/providers/user-provider.tsx +3 -2
  138. package/src/router/config.tsx +534 -0
  139. package/src/router/layouts/dashboard-layout.tsx +20 -0
  140. package/src/router/layouts/root-layout.tsx +69 -0
  141. package/src/router/loaders/auth-loader.ts +32 -0
  142. package/src/router/loaders/business-loader.ts +25 -0
  143. package/src/router/loaders/charge-loader.ts +25 -0
  144. package/src/router/loaders/index.ts +17 -0
  145. package/src/router/routes.ts +88 -0
  146. package/src/router/types.ts +62 -0
  147. package/dist/assets/index-0eCf1BcD.css +0 -1
  148. package/dist/assets/index-DHTbHvtz.js +0 -1188
  149. package/src/components/common/buttons/button-with-label.tsx +0 -41
  150. package/src/components/common/buttons/button.tsx +0 -44
@@ -1,5 +1,5 @@
1
1
  import { type ReactElement } from 'react';
2
- import { NavLink } from '@mantine/core';
2
+ import { Link } from 'react-router-dom';
3
3
  import { ChargesTableBusinessTripFieldsFragmentDoc } from '../../../gql/graphql.js';
4
4
  import { getFragmentData, type FragmentType } from '../../../gql/index.js';
5
5
 
@@ -29,17 +29,15 @@ export const BusinessTrip = ({ data }: Props): ReactElement => {
29
29
 
30
30
  return (
31
31
  <td>
32
- <a
33
- href={`/business-trips/${charge.businessTrip?.id}`}
32
+ <Link
33
+ to={`/business-trips/${charge.businessTrip?.id}`}
34
34
  target="_blank"
35
35
  rel="noreferrer"
36
36
  onClick={event => event.stopPropagation()}
37
+ className="inline-flex items-center font-semibold"
37
38
  >
38
- <NavLink
39
- label={charge.businessTrip?.name}
40
- className="[&>*>.mantine-NavLink-label]:font-semibold"
41
- />
42
- </a>
39
+ {charge.businessTrip?.name}
40
+ </Link>
43
41
  </td>
44
42
  );
45
43
  };
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo, type ReactElement } from 'react';
2
- import { Indicator, NavLink } from '@mantine/core';
2
+ import { Link } from 'react-router-dom';
3
+ import { Indicator } from '@mantine/core';
3
4
  import {
4
5
  ChargesTableEntityFieldsFragmentDoc,
5
6
  MissingChargeInfo,
@@ -81,14 +82,15 @@ export const Counterparty = ({ data }: Props): ReactElement => {
81
82
  <div className="flex flex-wrap">
82
83
  <Indicator inline size={12} disabled={!isError} color="red" zIndex="auto">
83
84
  {!isError && id && (
84
- <a
85
- href={getHref(id)}
85
+ <Link
86
+ to={getHref(id)}
86
87
  target="_blank"
87
88
  rel="noreferrer"
88
89
  onClick={event => event.stopPropagation()}
90
+ className="inline-flex items-center font-semibold"
89
91
  >
90
- <NavLink label={name} className="[&>*>.mantine-NavLink-label]:font-semibold" />
91
- </a>
92
+ {name}
93
+ </Link>
92
94
  )}
93
95
  {isError && name}
94
96
  </Indicator>
@@ -17,8 +17,8 @@ import { Dialog, DialogContent } from '../ui/dialog.js';
17
17
  interface ChargeExtendedInfoMenuProps {
18
18
  chargeId: string;
19
19
  chargeType: ChargesTableRowFieldsFragment['__typename'];
20
- setInsertDocument: () => void;
21
- setMatchDocuments: () => void;
20
+ setInsertDocument?: () => void;
21
+ setMatchDocuments?: () => void;
22
22
  onChange?: () => void;
23
23
  isIncome: boolean;
24
24
  }
@@ -78,16 +78,19 @@ export function ChargeExtendedInfoMenu({
78
78
  </ConfirmationModal>
79
79
  <Menu.Divider />
80
80
  <Menu.Label>Documents</Menu.Label>
81
- <Menu.Item
82
- icon={<ListPlus size={14} />}
83
- onClick={(event: ClickEvent): void => {
84
- event.stopPropagation();
85
- setInsertDocument();
86
- closeMenu();
87
- }}
88
- >
89
- Insert Document
90
- </Menu.Item>
81
+ {setInsertDocument && (
82
+ <Menu.Item
83
+ icon={<ListPlus size={14} />}
84
+ onClick={(event: ClickEvent): void => {
85
+ event.stopPropagation();
86
+ setInsertDocument();
87
+ closeMenu();
88
+ }}
89
+ >
90
+ Insert Document
91
+ </Menu.Item>
92
+ )}
93
+ (
91
94
  <Menu.Item
92
95
  icon={<FilePlus2 size={14} />}
93
96
  onClick={(event: ClickEvent): void => {
@@ -98,15 +101,18 @@ export function ChargeExtendedInfoMenu({
98
101
  >
99
102
  Upload Documents
100
103
  </Menu.Item>
101
- <Menu.Item
102
- icon={<Search size={14} />}
103
- onClick={(): void => {
104
- setMatchDocuments();
105
- closeMenu();
106
- }}
107
- >
108
- Match Document
109
- </Menu.Item>
104
+ )
105
+ {setMatchDocuments && (
106
+ <Menu.Item
107
+ icon={<Search size={14} />}
108
+ onClick={(): void => {
109
+ setMatchDocuments();
110
+ closeMenu();
111
+ }}
112
+ >
113
+ Match Document
114
+ </Menu.Item>
115
+ )}
110
116
  {isIncome && (
111
117
  <Menu.Item
112
118
  icon={<ListPlus size={14} />}
@@ -70,9 +70,9 @@ import { ChargeExtendedInfo } from './charge-extended-info.js';
70
70
  `;
71
71
 
72
72
  interface Props {
73
- setEditCharge: (onChange: () => void) => void;
74
- setInsertDocument: (onChange: () => void) => void;
75
- setMatchDocuments: () => void;
73
+ setEditCharge?: (onChange: () => void) => void;
74
+ setInsertDocument?: (onChange: () => void) => void;
75
+ setMatchDocuments?: () => void;
76
76
  toggleMergeCharge?: (onChange: () => void) => void;
77
77
  isSelectedForMerge: boolean;
78
78
  data: ChargesTableFieldsFragment;
@@ -198,12 +198,14 @@ export const ChargesTableRow = ({
198
198
 
199
199
  <td>
200
200
  <div className="flex flex-col gap-2">
201
- <EditMiniButton
202
- onClick={event => {
203
- event.stopPropagation();
204
- setEditCharge(onChange);
205
- }}
206
- />
201
+ {setEditCharge && (
202
+ <EditMiniButton
203
+ onClick={event => {
204
+ event.stopPropagation();
205
+ setEditCharge(onChange);
206
+ }}
207
+ />
208
+ )}
207
209
  {toggleMergeCharge && (
208
210
  <ToggleMergeSelected
209
211
  toggleMergeSelected={(): void => toggleMergeCharge(onChange)}
@@ -217,7 +219,7 @@ export const ChargesTableRow = ({
217
219
  <ChargeExtendedInfoMenu
218
220
  chargeId={charge.id}
219
221
  chargeType={charge.__typename}
220
- setInsertDocument={() => setInsertDocument(onChange)}
222
+ setInsertDocument={setInsertDocument ? () => setInsertDocument(onChange) : undefined}
221
223
  setMatchDocuments={setMatchDocuments}
222
224
  onChange={onChange}
223
225
  isIncome={isIncomeCharge}
@@ -16,9 +16,9 @@ import { ChargesTableRow } from './charges-row.jsx';
16
16
  `;
17
17
 
18
18
  interface Props {
19
- setEditChargeId: Dispatch<SetStateAction<{ id: string; onChange: () => void } | undefined>>;
20
- setInsertDocument: Dispatch<SetStateAction<{ id: string; onChange: () => void } | undefined>>;
21
- setMatchDocuments: Dispatch<
19
+ setEditChargeId?: Dispatch<SetStateAction<{ id: string; onChange: () => void } | undefined>>;
20
+ setInsertDocument?: Dispatch<SetStateAction<{ id: string; onChange: () => void } | undefined>>;
21
+ setMatchDocuments?: Dispatch<
22
22
  React.SetStateAction<
23
23
  | {
24
24
  id: string;
@@ -68,14 +68,20 @@ export const ChargesTable = ({
68
68
  <ChargesTableRow
69
69
  key={charge.id}
70
70
  data={charge}
71
- setEditCharge={(onChange: () => void): void =>
72
- setEditChargeId({ id: charge.id, onChange })
71
+ setEditCharge={
72
+ setEditChargeId
73
+ ? (onChange: () => void): void => setEditChargeId({ id: charge.id, onChange })
74
+ : undefined
73
75
  }
74
- setInsertDocument={(onChange: () => void): void =>
75
- setInsertDocument({ id: charge.id, onChange })
76
+ setInsertDocument={
77
+ setInsertDocument
78
+ ? (onChange: () => void): void => setInsertDocument({ id: charge.id, onChange })
79
+ : undefined
76
80
  }
77
- setMatchDocuments={(): void =>
78
- setMatchDocuments({ id: charge.id, ownerId: charge.owner.id })
81
+ setMatchDocuments={
82
+ setMatchDocuments
83
+ ? (): void => setMatchDocuments({ id: charge.id, ownerId: charge.owner.id })
84
+ : undefined
79
85
  }
80
86
  toggleMergeCharge={
81
87
  toggleMergeCharge
@@ -0,0 +1,464 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { format } from 'date-fns';
3
+ import { Plus } from 'lucide-react';
4
+ import { useForm } from 'react-hook-form';
5
+ import { z } from 'zod';
6
+ import { Button } from '@/components/ui/button.js';
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from '@/components/ui/dialog.js';
16
+ import {
17
+ Form,
18
+ FormControl,
19
+ FormField,
20
+ FormItem,
21
+ FormLabel,
22
+ FormMessage,
23
+ } from '@/components/ui/form.js';
24
+ import { Input } from '@/components/ui/input.js';
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ } from '@/components/ui/select.js';
32
+ import { Switch } from '@/components/ui/switch.js';
33
+ import { Textarea } from '@/components/ui/textarea.js';
34
+ import { BillingCycle, Currency, DocumentType, Product, SubscriptionPlan } from '@/gql/graphql.js';
35
+ import {
36
+ getDocumentNameFromType,
37
+ standardBillingCycle,
38
+ standardPlan,
39
+ type TimelessDateString,
40
+ } from '@/helpers/index.js';
41
+ import { useCreateContract } from '@/hooks/use-create-contract.js';
42
+ import { useUpdateContract } from '@/hooks/use-update-contract.js';
43
+ import { zodResolver } from '@hookform/resolvers/zod';
44
+
45
+ const contractFormSchema = z.object({
46
+ id: z.uuid().optional(),
47
+ // TODO: activate this field later. requires additional backend support
48
+ // operationsLimit: z.number().optional(),
49
+ startDate: z.iso.date('Start date is required'),
50
+ endDate: z.iso.date('End date is required'),
51
+ po: z.string().optional(),
52
+ paymentAmount: z.number().min(0, 'Payment amount must be non-negative'),
53
+ paymentCurrency: z.enum(Object.values(Currency), 'Currency is required'),
54
+ productType: z.enum(Object.values(Product)).optional(),
55
+ msCloudLink: z.url().optional().or(z.literal('')),
56
+ billingCycle: z.enum(Object.values(BillingCycle)).optional(),
57
+ subscriptionPlan: z.enum(Object.values(SubscriptionPlan)).optional(),
58
+ isActive: z.boolean(),
59
+ defaultRemark: z.string().optional(),
60
+ defaultDocumentType: z.enum(Object.values(DocumentType)),
61
+ });
62
+
63
+ export type ContractFormValues = z.infer<typeof contractFormSchema>;
64
+
65
+ const newContractDefaultValues: ContractFormValues = {
66
+ // TODO: activate this field later. requires additional backend support
67
+ // operationsLimit: 0,
68
+ startDate: '',
69
+ endDate: '',
70
+ po: undefined,
71
+ paymentAmount: 0,
72
+ paymentCurrency: Currency.Usd,
73
+ productType: Product.Hive,
74
+ msCloudLink: undefined,
75
+ billingCycle: BillingCycle.Monthly,
76
+ subscriptionPlan: undefined,
77
+ isActive: true,
78
+ defaultRemark: undefined,
79
+ defaultDocumentType: DocumentType.Proforma,
80
+ };
81
+
82
+ interface Props {
83
+ clientId: string;
84
+ contract?: ContractFormValues | null;
85
+ onDone?: () => void;
86
+ }
87
+
88
+ export function ModifyContractDialog({ clientId, contract, onDone }: Props) {
89
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
90
+ const [editingContract, setEditingContract] = useState<ContractFormValues | null>(null);
91
+
92
+ const { updateContract, updating } = useUpdateContract();
93
+ const { createContract, creating } = useCreateContract();
94
+
95
+ const form = useForm<ContractFormValues>({
96
+ resolver: zodResolver(contractFormSchema),
97
+ defaultValues: contract || newContractDefaultValues,
98
+ });
99
+
100
+ useEffect(() => {
101
+ if (contract) {
102
+ setEditingContract(contract);
103
+ form.reset({
104
+ // TODO: activate this field later. requires additional backend support
105
+ // operationsLimit: contract.operationsLimit,
106
+ startDate: contract.startDate,
107
+ endDate: contract.endDate,
108
+ po: contract.po,
109
+ paymentAmount: contract.paymentAmount,
110
+ paymentCurrency: contract.paymentCurrency,
111
+ productType: contract.productType,
112
+ msCloudLink: contract.msCloudLink,
113
+ billingCycle: contract.billingCycle,
114
+ subscriptionPlan: contract.subscriptionPlan,
115
+ isActive: contract.isActive,
116
+ defaultRemark: contract.defaultRemark,
117
+ defaultDocumentType: contract.defaultDocumentType,
118
+ });
119
+ setIsDialogOpen(true);
120
+ }
121
+ }, [contract, form]);
122
+
123
+ const handleNew = () => {
124
+ setEditingContract(null);
125
+ form.reset(newContractDefaultValues);
126
+ setIsDialogOpen(true);
127
+ };
128
+
129
+ const onSubmit = useCallback(
130
+ async (values: ContractFormValues) => {
131
+ if (editingContract) {
132
+ // Handle contract update
133
+ await updateContract({
134
+ contractId: editingContract.id!,
135
+ input: {
136
+ amount: { raw: values.paymentAmount, currency: values.paymentCurrency },
137
+ billingCycle: values.billingCycle,
138
+ documentType: values.defaultDocumentType,
139
+ endDate: format(new Date(values.endDate), 'yyyy-MM-dd') as TimelessDateString,
140
+ isActive: values.isActive,
141
+ msCloud: values.msCloudLink,
142
+ plan: values.subscriptionPlan,
143
+ product: values.productType,
144
+ purchaseOrder: values.po,
145
+ remarks: values.defaultRemark,
146
+ startDate: format(new Date(values.startDate), 'yyyy-MM-dd') as TimelessDateString,
147
+ },
148
+ });
149
+ } else {
150
+ // Handle new contract creation
151
+ if (!values.billingCycle) {
152
+ form.setError('billingCycle', { message: 'Billing cycle is required' });
153
+ return;
154
+ }
155
+ await createContract({
156
+ input: {
157
+ clientId,
158
+ amount: { raw: values.paymentAmount, currency: values.paymentCurrency },
159
+ billingCycle: values.billingCycle,
160
+ documentType: values.defaultDocumentType,
161
+ endDate: format(new Date(values.endDate), 'yyyy-MM-dd') as TimelessDateString,
162
+ isActive: values.isActive,
163
+ msCloud: values.msCloudLink,
164
+ plan: values.subscriptionPlan,
165
+ product: values.productType,
166
+ purchaseOrder: values.po,
167
+ remarks: values.defaultRemark,
168
+ startDate: format(new Date(values.startDate), 'yyyy-MM-dd') as TimelessDateString,
169
+ },
170
+ });
171
+ }
172
+ setIsDialogOpen(false);
173
+ setEditingContract(null);
174
+ onDone?.();
175
+ },
176
+ [editingContract, onDone, createContract, updateContract, clientId, form],
177
+ );
178
+
179
+ return (
180
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
181
+ <DialogTrigger asChild>
182
+ <Button size="sm" onClick={handleNew}>
183
+ <Plus className="h-4 w-4 mr-2" />
184
+ New Contract
185
+ </Button>
186
+ </DialogTrigger>
187
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
188
+ <DialogHeader>
189
+ <DialogTitle>{editingContract ? 'Edit Contract' : 'Create New Contract'}</DialogTitle>
190
+ <DialogDescription>
191
+ {editingContract
192
+ ? 'Update contract details'
193
+ : 'Add a new contract with all required details'}
194
+ </DialogDescription>
195
+ </DialogHeader>
196
+ <Form {...form}>
197
+ <form onSubmit={form.handleSubmit(onSubmit)}>
198
+ <div className="grid gap-4 py-4">
199
+ <div className="grid gap-4 md:grid-cols-2">
200
+ {/* TODO: activate this field later. requires additional backend support */}
201
+ {/* <FormField
202
+ control={form.control}
203
+ name="operationsLimit"
204
+ render={({ field }) => (
205
+ <FormItem>
206
+ <FormLabel>Operations Limit</FormLabel>
207
+ <FormControl>
208
+ <Input type="number" placeholder="500" {...field} />
209
+ </FormControl>
210
+ <FormMessage />
211
+ </FormItem>
212
+ )}
213
+ /> */}
214
+ <FormField
215
+ control={form.control}
216
+ name="po"
217
+ render={({ field }) => (
218
+ <FormItem>
219
+ <FormLabel>PO Number</FormLabel>
220
+ <FormControl>
221
+ <Input placeholder="PO-2024-001" {...field} />
222
+ </FormControl>
223
+ <FormMessage />
224
+ </FormItem>
225
+ )}
226
+ />
227
+ </div>
228
+
229
+ <div className="grid gap-4 md:grid-cols-2">
230
+ <FormField
231
+ control={form.control}
232
+ name="startDate"
233
+ render={({ field: { onChange, ...field } }) => (
234
+ <FormItem>
235
+ <FormLabel>Start Date</FormLabel>
236
+ <FormControl>
237
+ <Input type="date" {...field} onInput={date => onChange(date)} />
238
+ </FormControl>
239
+ <FormMessage />
240
+ </FormItem>
241
+ )}
242
+ />
243
+ <FormField
244
+ control={form.control}
245
+ name="endDate"
246
+ render={({ field: { onChange, ...field } }) => (
247
+ <FormItem>
248
+ <FormLabel>End Date</FormLabel>
249
+ <FormControl>
250
+ <Input type="date" {...field} onInput={date => onChange(date)} />
251
+ </FormControl>
252
+ <FormMessage />
253
+ </FormItem>
254
+ )}
255
+ />
256
+ </div>
257
+
258
+ <div className="grid gap-4 md:grid-cols-3">
259
+ <FormField
260
+ control={form.control}
261
+ name="paymentAmount"
262
+ render={({ field }) => (
263
+ <FormItem className="md:col-span-2">
264
+ <FormLabel>Payment Amount</FormLabel>
265
+ <FormControl>
266
+ <Input
267
+ type="number"
268
+ placeholder="24000"
269
+ {...field}
270
+ onChange={event => {
271
+ field.onChange(
272
+ event?.target.value ? Number(event?.target.value) : undefined,
273
+ );
274
+ }}
275
+ />
276
+ </FormControl>
277
+ <FormMessage />
278
+ </FormItem>
279
+ )}
280
+ />
281
+ <FormField
282
+ control={form.control}
283
+ name="paymentCurrency"
284
+ render={({ field }) => (
285
+ <FormItem>
286
+ <FormLabel>Currency</FormLabel>
287
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
288
+ <FormControl>
289
+ <SelectTrigger>
290
+ <SelectValue />
291
+ </SelectTrigger>
292
+ </FormControl>
293
+ <SelectContent>
294
+ {Object.values(Currency).map(currency => (
295
+ <SelectItem key={currency} value={currency}>
296
+ {currency}
297
+ </SelectItem>
298
+ ))}
299
+ </SelectContent>
300
+ </Select>
301
+ <FormMessage />
302
+ </FormItem>
303
+ )}
304
+ />
305
+ </div>
306
+
307
+ <div className="grid gap-4 md:grid-cols-2">
308
+ <FormField
309
+ control={form.control}
310
+ name="productType"
311
+ render={({ field }) => (
312
+ <FormItem>
313
+ <FormLabel>Product Type</FormLabel>
314
+ <FormControl>
315
+ <Input placeholder="Cloud Services" {...field} />
316
+ </FormControl>
317
+ <FormMessage />
318
+ </FormItem>
319
+ )}
320
+ />
321
+ <FormField
322
+ control={form.control}
323
+ name="billingCycle"
324
+ render={({ field }) => (
325
+ <FormItem>
326
+ <FormLabel>Billing Cycle</FormLabel>
327
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
328
+ <FormControl>
329
+ <SelectTrigger>
330
+ <SelectValue />
331
+ </SelectTrigger>
332
+ </FormControl>
333
+ <SelectContent>
334
+ {Object.values(BillingCycle).map(cycle => (
335
+ <SelectItem key={cycle} value={cycle}>
336
+ {standardBillingCycle(cycle)}
337
+ </SelectItem>
338
+ ))}
339
+ </SelectContent>
340
+ </Select>
341
+ <FormMessage />
342
+ </FormItem>
343
+ )}
344
+ />
345
+ </div>
346
+
347
+ <div className="grid gap-4 md:grid-cols-2">
348
+ <FormField
349
+ control={form.control}
350
+ name="subscriptionPlan"
351
+ render={({ field }) => (
352
+ <FormItem>
353
+ <FormLabel>Subscription Plan</FormLabel>
354
+ <FormControl>
355
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
356
+ <FormControl>
357
+ <SelectTrigger>
358
+ <SelectValue />
359
+ </SelectTrigger>
360
+ </FormControl>
361
+ <SelectContent>
362
+ {Object.values(SubscriptionPlan).map(plan => (
363
+ <SelectItem key={plan} value={plan}>
364
+ {standardPlan(plan)}
365
+ </SelectItem>
366
+ ))}
367
+ </SelectContent>
368
+ </Select>
369
+ </FormControl>
370
+ <FormMessage />
371
+ </FormItem>
372
+ )}
373
+ />
374
+ <FormField
375
+ control={form.control}
376
+ name="isActive"
377
+ render={({ field }) => (
378
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
379
+ <div className="space-y-0.5">
380
+ <FormLabel className="text-base">Active Status</FormLabel>
381
+ </div>
382
+ <FormControl>
383
+ <Switch checked={field.value} onCheckedChange={field.onChange} />
384
+ </FormControl>
385
+ </FormItem>
386
+ )}
387
+ />
388
+ </div>
389
+
390
+ <FormField
391
+ control={form.control}
392
+ name="msCloudLink"
393
+ render={({ field }) => (
394
+ <FormItem>
395
+ <FormLabel>MS Cloud Link</FormLabel>
396
+ <FormControl>
397
+ <Input
398
+ type="url"
399
+ placeholder="https://portal.azure.com/contract-id"
400
+ {...field}
401
+ />
402
+ </FormControl>
403
+ <FormMessage />
404
+ </FormItem>
405
+ )}
406
+ />
407
+
408
+ <FormField
409
+ control={form.control}
410
+ name="defaultDocumentType"
411
+ render={({ field }) => (
412
+ <FormItem>
413
+ <FormLabel>Default Document Type</FormLabel>
414
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
415
+ <FormControl>
416
+ <SelectTrigger>
417
+ <SelectValue />
418
+ </SelectTrigger>
419
+ </FormControl>
420
+ <SelectContent>
421
+ {Object.values(DocumentType).map(type => (
422
+ <SelectItem key={type} value={type}>
423
+ {getDocumentNameFromType(type)}
424
+ </SelectItem>
425
+ ))}
426
+ </SelectContent>
427
+ </Select>
428
+ <FormMessage />
429
+ </FormItem>
430
+ )}
431
+ />
432
+
433
+ <FormField
434
+ control={form.control}
435
+ name="defaultRemark"
436
+ render={({ field }) => (
437
+ <FormItem>
438
+ <FormLabel>Default Remark</FormLabel>
439
+ <FormControl>
440
+ <Textarea
441
+ placeholder="Enter default remark for this contract..."
442
+ rows={3}
443
+ {...field}
444
+ />
445
+ </FormControl>
446
+ <FormMessage />
447
+ </FormItem>
448
+ )}
449
+ />
450
+ </div>
451
+ <DialogFooter>
452
+ <Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
453
+ Cancel
454
+ </Button>
455
+ <Button type="submit" disabled={creating || updating}>
456
+ {editingContract ? 'Update Contract' : 'Create Contract'}
457
+ </Button>
458
+ </DialogFooter>
459
+ </form>
460
+ </Form>
461
+ </DialogContent>
462
+ </Dialog>
463
+ );
464
+ }