@accounter/client 0.0.8-alpha-20251030162201-d2f279aafe537912ec3546af855cbd3a38ac0f5c → 0.0.8-alpha-20251030164843-d2f490daba879840366288d1f62974ab25081876

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 (116) hide show
  1. package/CHANGELOG.md +27 -16
  2. package/dist/assets/{Checkbox-qEdmT4HE.js → Checkbox-dSmM5Kto.js} +2 -2
  3. package/dist/assets/{MultiSelect-Dm9mO7pJ.js → MultiSelect-BKzjkxRf.js} +1 -1
  4. package/dist/assets/{Progress-kr0gnfBa.js → Progress-CaCuNARE.js} +1 -1
  5. package/dist/assets/{Table-DnHfGe-y.js → Table-DxJ1YCr5.js} +1 -1
  6. package/dist/assets/{Typography-Cu3puAUK.js → Typography-rzf7AKzm.js} +1 -1
  7. package/dist/assets/{YearPickerInput-oa86GF4P.js → YearPickerInput-Bp2ytdrx.js} +1 -1
  8. package/dist/assets/{accordion-DLtMSqIv.js → accordion-B83CGJPM.js} +1 -1
  9. package/dist/assets/{accountant-approvals-DsL0uiBa.js → accountant-approvals-DRr7UYE4.js} +1 -1
  10. package/dist/assets/{accounter-table-BL_yrGIl.js → accounter-table-CLZmYCK0.js} +1 -1
  11. package/dist/assets/{addDays-DqZEDZy3.js → addDays-DbkSn4rN.js} +1 -1
  12. package/dist/assets/{all-charges-D3Xf-pEY.js → all-charges-Cn0oGAAS.js} +1 -1
  13. package/dist/assets/{arrow-up-down-DchtBEG4.js → arrow-up-down-kJRuABuk.js} +1 -1
  14. package/dist/assets/{building-2-ZTKteVvf.js → building-2-DAb5H_9e.js} +1 -1
  15. package/dist/assets/business-DeId1elJ.js +37 -0
  16. package/dist/assets/{business-extended-info-1mzdXJbs.js → business-extended-info-u-wZchjH.js} +4 -4
  17. package/dist/assets/{business-header-CJqfKV4d.js → business-header-vz6RQ5nZ.js} +1 -1
  18. package/dist/assets/{business-ledger-filters-BRB1r_DP.js → business-ledger-filters-DyyAMsOt.js} +1 -1
  19. package/dist/assets/{business-ledger-single-C8FIQJ8V.js → business-ledger-single-Cy7nqs_T.js} +1 -1
  20. package/dist/assets/business-trip-CA4-OIFH.js +1 -0
  21. package/dist/assets/{charge-Df3lWZH-.js → charge-DkSqluIG.js} +1 -1
  22. package/dist/assets/charges-filters-Djo_kWBf.js +1 -0
  23. package/dist/assets/{charges-ledger-validation-CtCBR3C-.js → charges-ledger-validation-DvCBQB1e.js} +1 -1
  24. package/dist/assets/{charges-table-CWbe3t13.js → charges-table-D1vDT-UP.js} +7 -7
  25. package/dist/assets/{chart-Cmm-jp0c.js → chart-BMSKXrwP.js} +1 -1
  26. package/dist/assets/contracts-ZJWgQOle.js +16 -0
  27. package/dist/assets/{data-table-pagination-fdOZTf64.js → data-table-pagination-BUpZwYMw.js} +2 -2
  28. package/dist/assets/{download-csv-button-CwK31swX.js → download-csv-button-tWswwko2.js} +1 -1
  29. package/dist/assets/edit-issue-document-modal-CHR5idQt.js +1 -0
  30. package/dist/assets/{editable-business-trip-Bjxkm9xV.js → editable-business-trip-D0UrHlFI.js} +2 -2
  31. package/dist/assets/eye-off-CGW7uC-c.js +6 -0
  32. package/dist/assets/{funnel-CMG4QvaK.js → funnel-Bmrsutts.js} +1 -1
  33. package/dist/assets/{index-CgokSIpu.js → index-2vL0dn5v.js} +1 -1
  34. package/dist/assets/index-B5HkjHjI.js +1 -0
  35. package/dist/assets/{index-Cw_aKL-8.js → index-BZ8AOwEl.js} +178 -173
  36. package/dist/assets/{index-CTatkdRy.js → index-Babx_Mc7.js} +2 -2
  37. package/dist/assets/{index-DLwMwjDQ.js → index-BnhjaCUg.js} +1 -1
  38. package/dist/assets/{index-xxe8M91O.js → index-Br0GTPfc.js} +2 -2
  39. package/dist/assets/index-BrSZxw1u.css +1 -0
  40. package/dist/assets/{index-DInU4RP4.js → index-C-VKA9Ok.js} +2 -2
  41. package/dist/assets/{index-CriBDqbK.js → index-C7cxxohd.js} +1 -1
  42. package/dist/assets/{index-C7oYbGGm.js → index-CUveXkuD.js} +1 -1
  43. package/dist/assets/index-CYX2yO-u.js +1 -0
  44. package/dist/assets/index-D1DOb6_Y.js +1 -0
  45. package/dist/assets/index-D7_jh5GD.js +1 -0
  46. package/dist/assets/index-DSuF8jdU.js +1 -0
  47. package/dist/assets/{index-CjC_SXIA.js → index-DTcOeWYo.js} +1 -1
  48. package/dist/assets/index-Dh1--D4F.js +1 -0
  49. package/dist/assets/{index-BEahR51x.js → index-Dkpu6Zhk.js} +2 -2
  50. package/dist/assets/index-DyH8FMHQ.js +1 -0
  51. package/dist/assets/index-HH5Rxplr.js +1 -0
  52. package/dist/assets/{index-B9CbQIGq.js → index-KBivZEq1.js} +2 -2
  53. package/dist/assets/{index-C_ZNQ0Zr.js → index-NS4xXWXV.js} +1 -1
  54. package/dist/assets/{index-6G62OvEh.js → index-cC755YI-.js} +1 -1
  55. package/dist/assets/{index-DFW4nqx8.js → index-dSI-XOHn.js} +7 -7
  56. package/dist/assets/index-h3vstU9e.js +1 -0
  57. package/dist/assets/{index-BZVrGMgV.js → index-tBA1vG1Y.js} +2 -2
  58. package/dist/assets/{index-jCPWIpdP.js → index-tMg1Ls6M.js} +1 -1
  59. package/dist/assets/{index.es-B_Bf6nNm.js → index.es-DNpMwt0n.js} +1 -1
  60. package/dist/assets/{insert-business-trip-modal-Ds3RIlmF.js → insert-business-trip-modal-4vXkqVnt.js} +2 -2
  61. package/dist/assets/issue-document-BoFHPotT.js +1 -0
  62. package/dist/assets/{list-plus-B3EB-dKe.js → list-plus-CDqpqAwS.js} +1 -1
  63. package/dist/assets/login-page-Bi8pRR_Y.js +1 -0
  64. package/dist/assets/{match-document-modal-Bo9pBr8W.js → match-document-modal-CETycNjj.js} +4 -4
  65. package/dist/assets/{missing-info-charges-BQYXI7bj.js → missing-info-charges-BPeEhfPv.js} +1 -1
  66. package/dist/assets/{modal-Pj_6bAFk.js → modal-8mlzJehM.js} +1 -1
  67. package/dist/assets/modify-contract-dialog-DWLEVQ1d.js +1 -0
  68. package/dist/assets/{page-layout-CVTeUSZp.js → page-layout-BfCdWwql.js} +1 -1
  69. package/dist/assets/{page-not-found-TvTAiAs4.js → page-not-found-DukC1kEI.js} +1 -1
  70. package/dist/assets/{panel-top-open-DzBY_TuM.js → panel-top-open-CpMFDJrl.js} +1 -1
  71. package/dist/assets/{pencil-BBmFhuZN.js → pencil-R8t4mFRO.js} +1 -1
  72. package/dist/assets/{report-commentary-row-Dqw62LLo.js → report-commentary-row-Byk41KMN.js} +1 -1
  73. package/dist/assets/{save-BW18gNVs.js → save-BjBw7mG5.js} +1 -1
  74. package/dist/assets/similar-transactions-modal-BPFaQ7dD.js +1 -0
  75. package/dist/assets/sub-dol6DnIB.js +1 -0
  76. package/dist/assets/subMonths-Cu51bw_I.js +1 -0
  77. package/dist/assets/{summary-Bbsq64IN.js → summary-Sj8NMKlv.js} +1 -1
  78. package/dist/assets/{toggle-expansion-button-BrKkZtGj.js → toggle-expansion-button-Buzq87-g.js} +1 -1
  79. package/dist/assets/tooltip-gvPoRI4e.js +1 -0
  80. package/dist/assets/{use-url-query-C0mVgO2i.js → use-url-query-DOEL3cY1.js} +1 -1
  81. package/dist/index.html +2 -2
  82. package/package.json +1 -1
  83. package/src/components/business/admin-business-section.tsx +103 -282
  84. package/src/components/clients/contracts/modify-contract-dialog.tsx +335 -253
  85. package/src/components/contracts/cells/client.tsx +24 -0
  86. package/src/components/contracts/cells/date.tsx +14 -0
  87. package/src/components/contracts/cells/index.ts +2 -0
  88. package/src/components/contracts/columns.tsx +182 -0
  89. package/src/components/contracts/contracts-filter.tsx +159 -0
  90. package/src/components/contracts/index.tsx +263 -0
  91. package/src/components/contracts/issue-documents-modal.tsx +236 -0
  92. package/src/components/layout/sidelinks.tsx +7 -0
  93. package/src/components/screens/businesses/clients/contracts/contracts.tsx +54 -0
  94. package/src/gql/gql.ts +27 -3
  95. package/src/gql/graphql.ts +73 -52
  96. package/src/router/config.tsx +13 -0
  97. package/src/router/routes.ts +1 -0
  98. package/dist/assets/business-CAcKSrnu.js +0 -37
  99. package/dist/assets/business-trip-DkTEY8b1.js +0 -1
  100. package/dist/assets/charges-filters-DUuY-N0H.js +0 -1
  101. package/dist/assets/index-BD4s2ucv.css +0 -1
  102. package/dist/assets/index-BFwWHKS1.js +0 -1
  103. package/dist/assets/index-BI0XToIk.js +0 -1
  104. package/dist/assets/index-BQb_C-JL.js +0 -1
  105. package/dist/assets/index-CJQlHnFs.js +0 -1
  106. package/dist/assets/index-Cm6Blkgf.js +0 -1
  107. package/dist/assets/index-DJLQkpmI.js +0 -6
  108. package/dist/assets/index-DcwGbXWr.js +0 -1
  109. package/dist/assets/index-Dlj3jpuW.js +0 -1
  110. package/dist/assets/index-iya2tfvn.js +0 -1
  111. package/dist/assets/issue-document-_iHCFO9h.js +0 -1
  112. package/dist/assets/login-page-BtT_TCEJ.js +0 -1
  113. package/dist/assets/similar-transactions-modal-B39q1NLa.js +0 -1
  114. package/dist/assets/sub-DOjETdwl.js +0 -1
  115. package/dist/assets/subMonths-DqeFZ4O6.js +0 -1
  116. package/dist/assets/tooltip-CbnJYNc4.js +0 -1
@@ -0,0 +1,24 @@
1
+ import { type ReactElement } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { ROUTES } from '@/router/routes.js';
4
+
5
+ type Props = {
6
+ id: string;
7
+ name: string;
8
+ };
9
+
10
+ export function Client({ id, name }: Props): ReactElement {
11
+ return (
12
+ <div className="flex flex-wrap flex-col justify-center">
13
+ <Link
14
+ to={ROUTES.BUSINESSES.DETAIL(id)}
15
+ target="_blank"
16
+ rel="noreferrer"
17
+ onClick={event => event.stopPropagation()}
18
+ className="inline-flex items-center font-semibold"
19
+ >
20
+ {name}
21
+ </Link>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { TimelessDateString } from '@/helpers/index.js';
3
+
4
+ type Props = {
5
+ timelessDate: TimelessDateString;
6
+ };
7
+
8
+ export const DateCell = ({ timelessDate }: Props): ReactElement => {
9
+ return (
10
+ <p className="text-sm font-medium">
11
+ {new Date(timelessDate).toLocaleDateString(undefined, { timeZone: 'UTC' })}
12
+ </p>
13
+ );
14
+ };
@@ -0,0 +1,2 @@
1
+ export { DateCell } from './date.js';
2
+ export { Client } from './client.js';
@@ -0,0 +1,182 @@
1
+ import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff, LinkIcon } from 'lucide-react';
2
+ import { Link } from 'react-router-dom';
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu.js';
10
+ import type { BillingCycle, SubscriptionPlan } from '@/gql/graphql.js';
11
+ import type { TimelessDateString } from '@/helpers/dates.js';
12
+ import { cn } from '@/lib/utils.js';
13
+ import type { Column, ColumnDef } from '@tanstack/react-table';
14
+ import { ModifyContractDialog } from '../clients/contracts/modify-contract-dialog.js';
15
+ import { Badge } from '../ui/badge.js';
16
+ import { Button } from '../ui/button.js';
17
+ import { Checkbox } from '../ui/checkbox.js';
18
+ import { Client, DateCell } from './cells/index.js';
19
+ import type { ContractRow } from './index.js';
20
+
21
+ interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
22
+ column: Column<TData, TValue>;
23
+ title: string;
24
+ }
25
+ export function DataTableColumnHeader<TData, TValue>({
26
+ column,
27
+ title,
28
+ className,
29
+ }: DataTableColumnHeaderProps<TData, TValue>) {
30
+ if (!column.getCanSort()) {
31
+ return <div className={cn(className)}>{title}</div>;
32
+ }
33
+ return (
34
+ <div className={cn('flex items-center gap-2', className)}>
35
+ <DropdownMenu>
36
+ <DropdownMenuTrigger asChild>
37
+ <Button variant="ghost" size="sm" className="data-[state=open]:bg-accent -ml-3 h-8">
38
+ <span>{title}</span>
39
+ {column.getIsSorted() === 'desc' ? (
40
+ <ArrowDown />
41
+ ) : column.getIsSorted() === 'asc' ? (
42
+ <ArrowUp />
43
+ ) : (
44
+ <ChevronsUpDown />
45
+ )}
46
+ </Button>
47
+ </DropdownMenuTrigger>
48
+ <DropdownMenuContent align="start">
49
+ <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
50
+ <ArrowUp />
51
+ Asc
52
+ </DropdownMenuItem>
53
+ <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
54
+ <ArrowDown />
55
+ Desc
56
+ </DropdownMenuItem>
57
+ <DropdownMenuSeparator />
58
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
59
+ <EyeOff />
60
+ Hide
61
+ </DropdownMenuItem>
62
+ </DropdownMenuContent>
63
+ </DropdownMenu>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export const columns: ColumnDef<ContractRow>[] = [
69
+ {
70
+ id: 'select',
71
+ header: ({ table }) => (
72
+ <Checkbox
73
+ checked={
74
+ table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')
75
+ }
76
+ onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
77
+ aria-label="Select all"
78
+ />
79
+ ),
80
+ cell: ({ row }) => (
81
+ <Checkbox
82
+ checked={row.getIsSelected()}
83
+ onCheckedChange={value => row.toggleSelected(!!value)}
84
+ aria-label="Select row"
85
+ />
86
+ ),
87
+ enableSorting: false,
88
+ enableHiding: false,
89
+ },
90
+ {
91
+ accessorKey: 'isActive',
92
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Is Active" />,
93
+ cell: ({ row }) => {
94
+ const isActive = row.getValue<boolean>('isActive');
95
+ return isActive ? (
96
+ <Badge variant="default">Active</Badge>
97
+ ) : (
98
+ <Badge variant="destructive">Inactive</Badge>
99
+ );
100
+ },
101
+ },
102
+ {
103
+ accessorKey: 'client.name',
104
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Client" />,
105
+ cell: ({ row }) => {
106
+ return <Client id={row.original.client.id} name={row.original.client.name} />;
107
+ },
108
+ },
109
+ {
110
+ accessorKey: 'startDate',
111
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Start" />,
112
+ cell: ({ row }) => <DateCell timelessDate={row.getValue<TimelessDateString>('startDate')} />,
113
+ },
114
+ {
115
+ accessorKey: 'endDate',
116
+ header: ({ column }) => <DataTableColumnHeader column={column} title="End" />,
117
+ cell: ({ row }) => <DateCell timelessDate={row.getValue<TimelessDateString>('endDate')} />,
118
+ },
119
+ {
120
+ accessorKey: 'purchaseOrder',
121
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Purchase Order" />,
122
+ cell: ({ row }) => (
123
+ <div className="flex flex-row gap-1 items-center">
124
+ {row.original.msCloud && (
125
+ <Link to={row.original.msCloud} target="_blank" rel="noreferrer" className="size-8">
126
+ <Button variant="link" size="sm">
127
+ <LinkIcon className="size-4" />
128
+ </Button>
129
+ </Link>
130
+ )}
131
+ <span className="text-sm font-medium">{row.getValue<string>('purchaseOrder')}</span>
132
+ </div>
133
+ ),
134
+ },
135
+ {
136
+ accessorKey: 'product',
137
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Product" />,
138
+ cell: ({ row }) => <p className="text-sm font-medium">{row.getValue<string>('product')}</p>,
139
+ },
140
+ {
141
+ accessorKey: 'plan',
142
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Subscription Plan" />,
143
+ cell: ({ row }) => (
144
+ <p className="text-sm font-medium">{row.getValue<SubscriptionPlan>('plan')}</p>
145
+ ),
146
+ },
147
+ {
148
+ accessorKey: 'billingCycle',
149
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Billing Cycle" />,
150
+ cell: ({ row }) => (
151
+ <p className="text-sm font-medium">{row.getValue<BillingCycle>('billingCycle')}</p>
152
+ ),
153
+ },
154
+ {
155
+ accessorKey: 'amount.raw',
156
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Amount" />,
157
+ cell: ({ row }) => <p className="text-sm font-medium">{row.original.amount.formatted}</p>,
158
+ },
159
+ {
160
+ accessorKey: 'operationsLimit',
161
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Operations Limit" />,
162
+ cell: ({ row }) => {
163
+ const operationsLimit = row.getValue<number>('operationsLimit');
164
+ if (!operationsLimit) {
165
+ return null;
166
+ }
167
+ return <p className="text-sm font-medium">{operationsLimit}</p>;
168
+ },
169
+ },
170
+ {
171
+ accessorKey: 'edit',
172
+ header: '',
173
+ cell: ({ row }) => (
174
+ <ModifyContractDialog
175
+ clientId={row.original.client.id}
176
+ contractId={row.original.id}
177
+ // onDone={refetch}
178
+ />
179
+ ),
180
+ enableSorting: false,
181
+ },
182
+ ];
@@ -0,0 +1,159 @@
1
+ import { useState } from 'react';
2
+ import { Filter } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button.js';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from '@/components/ui/dialog.js';
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '@/components/ui/select.js';
19
+ import { BillingCycle, Product, SubscriptionPlan } from '@/gql/graphql.js';
20
+ import { standardBillingCycle, standardPlan } from '@/helpers/index.js';
21
+ import type { Table } from '@tanstack/react-table';
22
+ import { Label } from '../ui/label.js';
23
+ import type { ContractRow } from './index.js';
24
+
25
+ interface Props {
26
+ table: Table<ContractRow>;
27
+ }
28
+
29
+ export function ContractsFilter({ table }: Props) {
30
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
31
+
32
+ return (
33
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
34
+ <DialogTrigger asChild>
35
+ <Button size="sm" variant="outline">
36
+ <Filter className="size-4" />
37
+ </Button>
38
+ </DialogTrigger>
39
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
40
+ <DialogHeader>
41
+ <DialogTitle>Filter Contracts</DialogTitle>
42
+ <DialogDescription>Filter contracts based on various criteria</DialogDescription>
43
+ </DialogHeader>
44
+ <div className="grid gap-4 py-4">
45
+ <div className="grid gap-4 md:grid-cols-2">
46
+ <div className="space-y-2">
47
+ <Label>Product Type</Label>
48
+ <Select
49
+ onValueChange={value =>
50
+ table.getColumn('product')?.setFilterValue(value === 'NULL' ? undefined : value)
51
+ }
52
+ value={table.getColumn('product')?.getFilterValue() as Product | ''}
53
+ >
54
+ <SelectTrigger className="w-full">
55
+ <SelectValue />
56
+ </SelectTrigger>
57
+ <SelectContent>
58
+ <SelectItem value="NULL" className="font-light text-xs">
59
+ (None)
60
+ </SelectItem>
61
+ {Object.values(Product).map(product => (
62
+ <SelectItem key={product} value={product}>
63
+ {product}
64
+ </SelectItem>
65
+ ))}
66
+ </SelectContent>
67
+ </Select>
68
+ </div>
69
+ <div className="space-y-2">
70
+ <Label>Billing Cycle</Label>
71
+ <Select
72
+ onValueChange={value =>
73
+ table
74
+ .getColumn('billingCycle')
75
+ ?.setFilterValue(value === 'NULL' ? undefined : value)
76
+ }
77
+ value={table.getColumn('billingCycle')?.getFilterValue() as BillingCycle | ''}
78
+ >
79
+ <SelectTrigger className="w-full">
80
+ <SelectValue />
81
+ </SelectTrigger>
82
+ <SelectContent>
83
+ <SelectItem value="NULL" className="font-light text-xs">
84
+ (None)
85
+ </SelectItem>
86
+ {Object.values(BillingCycle).map(cycle => (
87
+ <SelectItem key={cycle} value={cycle}>
88
+ {standardBillingCycle(cycle)}
89
+ </SelectItem>
90
+ ))}
91
+ </SelectContent>
92
+ </Select>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="grid gap-4 md:grid-cols-2">
97
+ <div className="space-y-2">
98
+ <Label>Subscription Plan</Label>
99
+ <Select
100
+ onValueChange={value =>
101
+ table.getColumn('plan')?.setFilterValue(value === 'NULL' ? undefined : value)
102
+ }
103
+ value={table.getColumn('plan')?.getFilterValue() as SubscriptionPlan | ''}
104
+ >
105
+ <SelectTrigger className="w-full">
106
+ <SelectValue />
107
+ </SelectTrigger>
108
+ <SelectContent>
109
+ <SelectItem value="NULL" className="font-light text-xs">
110
+ (None)
111
+ </SelectItem>
112
+ {Object.values(SubscriptionPlan).map(plan => (
113
+ <SelectItem key={plan} value={plan}>
114
+ {standardPlan(plan)}
115
+ </SelectItem>
116
+ ))}
117
+ </SelectContent>
118
+ </Select>
119
+ </div>
120
+
121
+ <div className="space-y-2">
122
+ <Label>Is Active</Label>
123
+ <Select
124
+ onValueChange={value =>
125
+ table
126
+ .getColumn('isActive')
127
+ ?.setFilterValue(
128
+ value === 'NULL' ? undefined : value === 'active' ? true : false,
129
+ )
130
+ }
131
+ value={convertBooleanToString(
132
+ table.getColumn('isActive')?.getFilterValue() as boolean | undefined,
133
+ )}
134
+ >
135
+ <SelectTrigger className="w-full">
136
+ <SelectValue />
137
+ </SelectTrigger>
138
+ <SelectContent>
139
+ <SelectItem value="NULL" className="font-light text-xs">
140
+ (None)
141
+ </SelectItem>
142
+ <SelectItem value="active">Active</SelectItem>
143
+ <SelectItem value="inactive">Inactive</SelectItem>
144
+ </SelectContent>
145
+ </Select>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </DialogContent>
150
+ </Dialog>
151
+ );
152
+ }
153
+
154
+ function convertBooleanToString(value: boolean | undefined): string {
155
+ if (value === undefined) {
156
+ return 'NULL';
157
+ }
158
+ return value ? 'active' : 'inactive';
159
+ }
@@ -0,0 +1,263 @@
1
+ import { useEffect, useMemo, useState, type ReactElement } from 'react';
2
+ import { ChevronDown } from 'lucide-react';
3
+ import { Pagination } from '@/components/common/index.js';
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@/components/ui/table.js';
12
+ import { ContractForContractsTableFieldsFragmentDoc } from '@/gql/graphql.js';
13
+ import { getFragmentData, type FragmentType } from '@/gql/index.js';
14
+ import type { TimelessDateString } from '@/helpers/dates.js';
15
+ import {
16
+ flexRender,
17
+ getCoreRowModel,
18
+ getFilteredRowModel,
19
+ getPaginationRowModel,
20
+ getSortedRowModel,
21
+ useReactTable,
22
+ type ColumnFiltersState,
23
+ type SortingState,
24
+ } from '@tanstack/react-table';
25
+ import type { BillingCycle, Product, SubscriptionPlan } from '../../gql/graphql.js';
26
+ import { Button } from '../ui/button.js';
27
+ import {
28
+ DropdownMenu,
29
+ DropdownMenuCheckboxItem,
30
+ DropdownMenuContent,
31
+ DropdownMenuTrigger,
32
+ } from '../ui/dropdown-menu.js';
33
+ import { columns } from './columns.js';
34
+ import { ContractsFilter } from './contracts-filter.js';
35
+ import { IssueDocumentsModal } from './issue-documents-modal.js';
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
38
+ /* GraphQL */ `
39
+ fragment ContractForContractsTableFields on Contract {
40
+ id
41
+ isActive
42
+ client {
43
+ id
44
+ originalBusiness {
45
+ id
46
+ name
47
+ }
48
+ }
49
+ purchaseOrders
50
+ startDate
51
+ endDate
52
+ amount {
53
+ raw
54
+ formatted
55
+ }
56
+ billingCycle
57
+ product
58
+ plan
59
+ operationsLimit
60
+ msCloud
61
+ # documentType
62
+ # remarks
63
+ # operationsLimit
64
+ }
65
+ `;
66
+
67
+ export interface ContractRow {
68
+ id: string;
69
+ isActive: boolean;
70
+ client: {
71
+ id: string;
72
+ name: string;
73
+ };
74
+ purchaseOrder?: string;
75
+ startDate: TimelessDateString;
76
+ endDate: TimelessDateString;
77
+ amount: {
78
+ raw: number;
79
+ formatted: string;
80
+ };
81
+ billingCycle: BillingCycle;
82
+ product?: Product;
83
+ plan?: SubscriptionPlan;
84
+ operationsLimit?: number;
85
+ msCloud?: string;
86
+ // documentType
87
+ // remarks
88
+ // operationsLimit
89
+ }
90
+
91
+ function convertContractFragmentToTableRow(
92
+ data: FragmentType<typeof ContractForContractsTableFieldsFragmentDoc>,
93
+ ): ContractRow {
94
+ const fragmentData = getFragmentData(ContractForContractsTableFieldsFragmentDoc, data);
95
+ return {
96
+ id: fragmentData.id,
97
+ isActive: fragmentData.isActive,
98
+ client: {
99
+ id: fragmentData.client.id,
100
+ name: fragmentData.client.originalBusiness.name,
101
+ },
102
+ purchaseOrder: fragmentData.purchaseOrders[0],
103
+ startDate: fragmentData.startDate,
104
+ endDate: fragmentData.endDate,
105
+ amount: {
106
+ raw: fragmentData.amount.raw,
107
+ formatted: fragmentData.amount.formatted,
108
+ },
109
+ billingCycle: fragmentData.billingCycle,
110
+ product: fragmentData.product ?? undefined,
111
+ plan: fragmentData.plan ?? undefined,
112
+ operationsLimit: fragmentData.operationsLimit,
113
+ msCloud: fragmentData.msCloud?.toString() ?? undefined,
114
+ };
115
+ }
116
+
117
+ type Props = {
118
+ data: FragmentType<typeof ContractForContractsTableFieldsFragmentDoc>[];
119
+ onChange?: () => void;
120
+ };
121
+
122
+ export const ContractsTable = ({ data }: Props): ReactElement => {
123
+ const [sorting, setSorting] = useState<SortingState>([
124
+ {
125
+ id: 'endDate',
126
+ desc: true,
127
+ },
128
+ {
129
+ id: 'amount.raw',
130
+ desc: true,
131
+ },
132
+ ]);
133
+ const [rowSelection, setRowSelection] = useState({});
134
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
135
+ const [selectedContractIds, setSelectedContractIds] = useState<string[]>([]);
136
+
137
+ const contracts = useMemo(
138
+ () => data.map(rawContract => convertContractFragmentToTableRow(rawContract)),
139
+ [data],
140
+ );
141
+
142
+ const table = useReactTable({
143
+ data: contracts,
144
+ columns,
145
+ getCoreRowModel: getCoreRowModel(),
146
+ onSortingChange: setSorting,
147
+ getSortedRowModel: getSortedRowModel(),
148
+ getPaginationRowModel: getPaginationRowModel(),
149
+ onRowSelectionChange: setRowSelection,
150
+ onColumnFiltersChange: setColumnFilters,
151
+ getFilteredRowModel: getFilteredRowModel(),
152
+ state: {
153
+ sorting,
154
+ rowSelection,
155
+ columnFilters,
156
+ },
157
+ initialState: {
158
+ pagination: {
159
+ pageIndex: 0,
160
+ pageSize: 50,
161
+ },
162
+ },
163
+ });
164
+
165
+ useEffect(() => {
166
+ const selectedIndexes = Object.entries(rowSelection)
167
+ .filter(([, value]) => !!value)
168
+ .map(([key]) => Number(key));
169
+ const newSelectedContractIds = Array.from(
170
+ new Set(contracts.filter((_, i) => selectedIndexes.includes(i)).map(c => c.id)),
171
+ );
172
+ if (
173
+ selectedContractIds.length !== newSelectedContractIds.length ||
174
+ !selectedContractIds.every(id => newSelectedContractIds.includes(id))
175
+ ) {
176
+ setSelectedContractIds(newSelectedContractIds);
177
+ }
178
+ }, [rowSelection, contracts, selectedContractIds, columnFilters]);
179
+
180
+ return (
181
+ <div className="w-full">
182
+ <div className="flex items-center py-4 gap-4">
183
+ <ContractsFilter table={table} />
184
+ <DropdownMenu>
185
+ <DropdownMenuTrigger asChild>
186
+ <Button variant="outline">
187
+ Columns <ChevronDown />
188
+ </Button>
189
+ </DropdownMenuTrigger>
190
+ <DropdownMenuContent align="end">
191
+ {table
192
+ .getAllColumns()
193
+ .filter(column => column.getCanHide())
194
+ .map(column => {
195
+ return (
196
+ <DropdownMenuCheckboxItem
197
+ key={column.id}
198
+ className="capitalize"
199
+ checked={column.getIsVisible()}
200
+ onCheckedChange={value => column.toggleVisibility(!!value)}
201
+ >
202
+ {column.id}
203
+ </DropdownMenuCheckboxItem>
204
+ );
205
+ })}
206
+ </DropdownMenuContent>
207
+ </DropdownMenu>
208
+
209
+ <div className="ml-auto">
210
+ <IssueDocumentsModal contractIds={selectedContractIds} className="ml-auto" />
211
+ </div>
212
+ </div>
213
+ <div className="overflow-hidden rounded-md border">
214
+ <Table>
215
+ <TableHeader>
216
+ {table.getHeaderGroups().map(headerGroup => (
217
+ <TableRow key={headerGroup.id}>
218
+ {headerGroup.headers.map(header => (
219
+ <TableHead key={header.id} colSpan={header.colSpan}>
220
+ {header.isPlaceholder
221
+ ? null
222
+ : flexRender(header.column.columnDef.header, header.getContext())}
223
+ </TableHead>
224
+ ))}
225
+ </TableRow>
226
+ ))}
227
+ </TableHeader>
228
+ <TableBody>
229
+ {table.getRowModel().rows?.length ? (
230
+ table.getRowModel().rows.map(row => (
231
+ <TableRow key={row.id}>
232
+ {row.getVisibleCells().map(cell => (
233
+ <TableCell key={cell.id}>
234
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
235
+ </TableCell>
236
+ ))}
237
+ </TableRow>
238
+ ))
239
+ ) : (
240
+ <TableRow>
241
+ <TableCell colSpan={columns.length} className="h-24 text-center">
242
+ No results.
243
+ </TableCell>
244
+ </TableRow>
245
+ )}
246
+ </TableBody>
247
+ </Table>
248
+ </div>
249
+ <div className="flex items-center justify-end space-x-2 py-4">
250
+ <div className="text-muted-foreground flex-1 text-sm">
251
+ {table.getFilteredSelectedRowModel().rows.length} of{' '}
252
+ {table.getFilteredRowModel().rows.length} row(s) selected.
253
+ </div>
254
+ <Pagination
255
+ className="w-fit mx-0"
256
+ value={table.getState().pagination.pageIndex}
257
+ total={table.getPageCount()}
258
+ onChange={page => table.setPageIndex(page)}
259
+ />
260
+ </div>
261
+ </div>
262
+ );
263
+ };