@trustless-work/blocks 0.0.1

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 (74) hide show
  1. package/README.md +96 -0
  2. package/bin/index.js +1123 -0
  3. package/package.json +44 -0
  4. package/templates/deps.json +29 -0
  5. package/templates/escrows/details/Actions.tsx +149 -0
  6. package/templates/escrows/details/Entities.tsx +48 -0
  7. package/templates/escrows/details/EntityCard.tsx +98 -0
  8. package/templates/escrows/details/EscrowDetailDialog.tsx +154 -0
  9. package/templates/escrows/details/GeneralInformation.tsx +329 -0
  10. package/templates/escrows/details/MilestoneCard.tsx +254 -0
  11. package/templates/escrows/details/MilestoneDetailDialog.tsx +276 -0
  12. package/templates/escrows/details/Milestones.tsx +87 -0
  13. package/templates/escrows/details/ProgressEscrow.tsx +191 -0
  14. package/templates/escrows/details/StatisticsCard.tsx +79 -0
  15. package/templates/escrows/details/SuccessReleaseDialog.tsx +101 -0
  16. package/templates/escrows/details/useDetailsEscrow.ts +126 -0
  17. package/templates/escrows/escrow-context/EscrowAmountProvider.tsx +86 -0
  18. package/templates/escrows/escrow-context/EscrowDialogsProvider.tsx +108 -0
  19. package/templates/escrows/escrow-context/EscrowProvider.tsx +124 -0
  20. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +503 -0
  21. package/templates/escrows/escrows-by-role/cards/Filters.tsx +421 -0
  22. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +427 -0
  23. package/templates/escrows/escrows-by-role/table/Filters.tsx +421 -0
  24. package/templates/escrows/escrows-by-role/useEscrowsByRole.shared.ts +336 -0
  25. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +502 -0
  26. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +389 -0
  27. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +422 -0
  28. package/templates/escrows/escrows-by-signer/table/Filters.tsx +389 -0
  29. package/templates/escrows/escrows-by-signer/useEscrowsBySigner.shared.ts +320 -0
  30. package/templates/escrows/single-release/approve-milestone/button/ApproveMilestone.tsx +78 -0
  31. package/templates/escrows/single-release/approve-milestone/dialog/ApproveMilestone.tsx +102 -0
  32. package/templates/escrows/single-release/approve-milestone/form/ApproveMilestone.tsx +80 -0
  33. package/templates/escrows/single-release/approve-milestone/shared/schema.ts +9 -0
  34. package/templates/escrows/single-release/approve-milestone/shared/useApproveMilestone.ts +67 -0
  35. package/templates/escrows/single-release/change-milestone-status/button/ChangeMilestoneStatus.tsx +78 -0
  36. package/templates/escrows/single-release/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +167 -0
  37. package/templates/escrows/single-release/change-milestone-status/form/ChangeMilestoneStatus.tsx +114 -0
  38. package/templates/escrows/single-release/change-milestone-status/shared/schema.ts +15 -0
  39. package/templates/escrows/single-release/change-milestone-status/shared/useChangeMilestoneStatus.ts +77 -0
  40. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +68 -0
  41. package/templates/escrows/single-release/fund-escrow/button/FundEscrow.tsx +84 -0
  42. package/templates/escrows/single-release/fund-escrow/dialog/FundEscrow.tsx +77 -0
  43. package/templates/escrows/single-release/fund-escrow/form/FundEscrow.tsx +54 -0
  44. package/templates/escrows/single-release/fund-escrow/shared/schema.ts +10 -0
  45. package/templates/escrows/single-release/fund-escrow/shared/useFundEscrow.ts +66 -0
  46. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +526 -0
  47. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +504 -0
  48. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +232 -0
  49. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +115 -0
  50. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +80 -0
  51. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +94 -0
  52. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +123 -0
  53. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +82 -0
  54. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +82 -0
  55. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +58 -0
  56. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +485 -0
  57. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +463 -0
  58. package/templates/escrows/single-release/update-escrow/shared/schema.ts +139 -0
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +211 -0
  60. package/templates/handle-errors/errors.enum.ts +6 -0
  61. package/templates/handle-errors/handle.ts +47 -0
  62. package/templates/helpers/format.helper.ts +27 -0
  63. package/templates/helpers/useCopy.ts +13 -0
  64. package/templates/providers/ReactQueryClientProvider.tsx +28 -0
  65. package/templates/providers/TrustlessWork.tsx +30 -0
  66. package/templates/tanstak/useEscrowsByRoleQuery.ts +87 -0
  67. package/templates/tanstak/useEscrowsBySignerQuery.ts +78 -0
  68. package/templates/tanstak/useEscrowsMutations.ts +411 -0
  69. package/templates/wallet-kit/WalletButtons.tsx +116 -0
  70. package/templates/wallet-kit/WalletProvider.tsx +94 -0
  71. package/templates/wallet-kit/trustlines.ts +40 -0
  72. package/templates/wallet-kit/useWallet.ts +77 -0
  73. package/templates/wallet-kit/validators.ts +12 -0
  74. package/templates/wallet-kit/wallet-kit.ts +30 -0
@@ -0,0 +1,329 @@
1
+ "use client";
2
+
3
+ import React, { useMemo } from "react";
4
+ import { Card } from "__UI_BASE__/card";
5
+ import { cn } from "@/lib/utils";
6
+ import { MultiReleaseMilestone } from "@trustless-work/escrow";
7
+ import {
8
+ Ban,
9
+ CircleCheckBig,
10
+ CircleDollarSign,
11
+ Handshake,
12
+ Wallet,
13
+ Info,
14
+ Users,
15
+ Check,
16
+ Copy,
17
+ } from "lucide-react";
18
+ import { Actions, roleActions } from "./Actions";
19
+ import type {
20
+ DialogStates,
21
+ StatusStates,
22
+ } from "../../escrow-context/EscrowDialogsProvider";
23
+ import {
24
+ GetEscrowsFromIndexerResponse,
25
+ Role,
26
+ } from "@trustless-work/escrow/types";
27
+ import { useEscrowAmountContext } from "../../escrow-context/EscrowAmountProvider";
28
+ import { StatisticsCard } from "./StatisticsCard";
29
+ import {
30
+ formatAddress,
31
+ formatCurrency,
32
+ } from "@/components/tw-blocks/helpers/format.helper";
33
+ import { useCopy } from "@/components/tw-blocks/helpers/useCopy";
34
+
35
+ interface GeneralInformationProps {
36
+ selectedEscrow: GetEscrowsFromIndexerResponse;
37
+ userRolesInEscrow: string[];
38
+ dialogStates: DialogStates & StatusStates;
39
+ areAllMilestonesApproved: boolean;
40
+ activeRole: Role[];
41
+ }
42
+
43
+ export const GeneralInformation = ({
44
+ selectedEscrow,
45
+ userRolesInEscrow,
46
+ dialogStates,
47
+ areAllMilestonesApproved,
48
+ activeRole,
49
+ }: GeneralInformationProps) => {
50
+ const { trustlessWorkAmount, receiverAmount, platformFeeAmount } =
51
+ useEscrowAmountContext();
52
+ const { copiedKeyId, copyToClipboard } = useCopy();
53
+
54
+ const totalAmount = useMemo(() => {
55
+ if (!selectedEscrow) return 0;
56
+ const milestones = selectedEscrow.milestones as MultiReleaseMilestone[];
57
+ if (selectedEscrow?.type === "single-release") {
58
+ return selectedEscrow.amount;
59
+ } else {
60
+ return milestones.reduce(
61
+ (acc, milestone) => acc + Number(milestone.amount),
62
+ 0
63
+ );
64
+ }
65
+ }, [selectedEscrow]);
66
+
67
+ return (
68
+ <div className="space-y-6 h-full">
69
+ <div className="flex flex-col md:flex-row gap-4">
70
+ <div className="flex flex-col md:flex-row w-full mdw-4/5 gap-4">
71
+ {selectedEscrow.flags?.disputed && (
72
+ <StatisticsCard
73
+ title="Status"
74
+ icon={Ban}
75
+ iconColor="text-destructive"
76
+ value="In Dispute"
77
+ />
78
+ )}
79
+
80
+ {selectedEscrow.flags?.released && (
81
+ <StatisticsCard
82
+ title="Status"
83
+ icon={CircleCheckBig}
84
+ iconColor="text-green-800"
85
+ value="Released"
86
+ actionLabel="See Details"
87
+ onAction={() => dialogStates.successRelease.setIsOpen(true)}
88
+ />
89
+ )}
90
+
91
+ {selectedEscrow.flags?.resolved && (
92
+ <StatisticsCard
93
+ title="Status"
94
+ icon={Handshake}
95
+ iconColor="text-green-800"
96
+ value="Resolved"
97
+ />
98
+ )}
99
+
100
+ <StatisticsCard
101
+ title="Amount"
102
+ icon={CircleDollarSign}
103
+ value={formatCurrency(totalAmount, selectedEscrow.trustline?.name)}
104
+ />
105
+
106
+ <StatisticsCard
107
+ title="Balance"
108
+ icon={Wallet}
109
+ value={formatCurrency(
110
+ selectedEscrow.balance ?? 0,
111
+ selectedEscrow.trustline?.name
112
+ )}
113
+ />
114
+ </div>
115
+ <div className="flex w-full md:w-1/5">
116
+ <Actions
117
+ selectedEscrow={selectedEscrow}
118
+ userRolesInEscrow={userRolesInEscrow}
119
+ areAllMilestonesApproved={areAllMilestonesApproved}
120
+ activeRole={activeRole}
121
+ />
122
+ </div>
123
+ </div>
124
+ <div
125
+ className={cn(
126
+ "grid gap-6 h-full",
127
+ !selectedEscrow.flags?.released && !selectedEscrow.flags?.resolved
128
+ ? selectedEscrow?.type === "multi-release"
129
+ ? "grid-cols-1 md:grid-cols-1 mx-auto"
130
+ : "grid-cols-1 lg:grid-cols-4"
131
+ : "grid-cols-1 md:grid-cols-1 w-full mx-auto"
132
+ )}
133
+ >
134
+ <div className="lg:col-span-3">
135
+ <Card className="px-6 py-4 h-full">
136
+ <h3 className="text-lg font-semibold">Basic Information</h3>
137
+ <div className="grid gap-4">
138
+ <div className="p-4 bg-muted/50 rounded-lg border">
139
+ <div className="flex items-center gap-3">
140
+ <Info className="h-5 w-5 text-primary flex-shrink-0" />
141
+ <div className="flex-1 min-w-0">
142
+ <div className="flex items-center justify-between mb-2">
143
+ <span className="text-sm font-medium text-muted-foreground">
144
+ {selectedEscrow.trustline?.name || "No Trustline"} |
145
+ Escrow ID
146
+ </span>
147
+ <button
148
+ onClick={() =>
149
+ copyToClipboard(selectedEscrow?.contractId || "")
150
+ }
151
+ className="p-1.5 hover:bg-muted rounded-md transition-colors flex-shrink-0"
152
+ >
153
+ {copiedKeyId ? (
154
+ <Check className="h-4 w-4 text-green-700" />
155
+ ) : (
156
+ <Copy className="h-4 w-4" />
157
+ )}
158
+ </button>
159
+ </div>
160
+ <span className="font-mono text-sm break-all text-foreground">
161
+ {formatAddress(selectedEscrow.contractId || "")}
162
+ </span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
168
+ <div className="p-4 bg-muted/50 rounded-lg border">
169
+ <div className="flex items-center gap-3 mb-3">
170
+ <Users className="h-5 w-5 text-primary flex-shrink-0" />
171
+ <span className="text-sm font-medium text-muted-foreground">
172
+ Roles
173
+ </span>
174
+ </div>
175
+ <div className="flex flex-wrap gap-2">
176
+ {userRolesInEscrow.map((role) => {
177
+ const roleData = roleActions.find((r) => r.role === role);
178
+ return (
179
+ <div
180
+ key={role}
181
+ className="p-2 bg-primary/10 rounded-md hover:bg-primary/20 transition-colors"
182
+ >
183
+ {roleData?.icon || (
184
+ <Users className="h-4 w-4 text-primary" />
185
+ )}
186
+ </div>
187
+ );
188
+ })}
189
+ </div>
190
+ </div>
191
+
192
+ <div className="p-4 bg-muted/50 rounded-lg border">
193
+ <div className="flex items-center gap-3 mb-2">
194
+ <Info className="h-5 w-5 text-primary flex-shrink-0" />
195
+ <span className="text-sm font-medium text-muted-foreground">
196
+ Memo
197
+ </span>
198
+ </div>
199
+ <span className="font-medium text-foreground">
200
+ {selectedEscrow?.receiverMemo || "No Memo"}
201
+ </span>
202
+ </div>
203
+ </div>
204
+
205
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
206
+ <div className="p-4 bg-muted/50 rounded-lg border">
207
+ <div className="flex items-center gap-3 mb-2">
208
+ <CircleDollarSign className="h-5 w-5 text-primary flex-shrink-0" />
209
+ <span className="text-sm font-medium text-muted-foreground">
210
+ Engagement ID
211
+ </span>
212
+ </div>
213
+ <span className="font-medium text-foreground">
214
+ {selectedEscrow?.engagementId || "No Engagement"}
215
+ </span>
216
+ </div>
217
+
218
+ <div className="p-4 bg-muted/50 rounded-lg border">
219
+ <div className="flex items-center gap-3 mb-2">
220
+ <CircleDollarSign className="h-5 w-5 text-primary flex-shrink-0" />
221
+ <span className="text-sm font-medium text-muted-foreground">
222
+ Type
223
+ </span>
224
+ </div>
225
+ <span className="font-medium text-foreground">
226
+ {selectedEscrow?.type === "multi-release"
227
+ ? "Multi Release"
228
+ : "Single Release"}
229
+ </span>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </Card>
234
+ </div>
235
+
236
+ {selectedEscrow?.type !== "multi-release" &&
237
+ !selectedEscrow.flags?.released &&
238
+ !selectedEscrow.flags?.resolved && (
239
+ <div className="lg:col-span-1">
240
+ <Card className="p-4 h-full">
241
+ <h3 className="text-lg font-semibold">
242
+ Release Amount Distribution
243
+ </h3>
244
+ <div className="space-y-4">
245
+ <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border">
246
+ <div className="flex items-center gap-2">
247
+ <CircleDollarSign className="h-4 w-4 text-primary" />
248
+ <div className="flex flex-col">
249
+ <span className="text-sm text-muted-foreground">
250
+ Total Amount
251
+ </span>
252
+ <span className="font-medium">
253
+ {formatCurrency(
254
+ selectedEscrow.amount,
255
+ selectedEscrow.trustline?.name
256
+ )}
257
+ </span>
258
+ </div>
259
+ </div>
260
+ <div className="text-sm text-muted-foreground">100%</div>
261
+ </div>
262
+
263
+ <div className="space-y-2">
264
+ <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border">
265
+ <div className="flex items-center gap-2">
266
+ <Users className="h-4 w-4 text-primary" />
267
+ <div className="flex flex-col">
268
+ <span className="text-sm text-muted-foreground">
269
+ Receiver
270
+ </span>
271
+ <span className="font-medium">
272
+ {formatCurrency(
273
+ Number(receiverAmount.toFixed(2)),
274
+ selectedEscrow.trustline?.name
275
+ )}
276
+ </span>
277
+ </div>
278
+ </div>
279
+ <div className="text-sm text-muted-foreground">
280
+ {100 - selectedEscrow.platformFee - 0.3}%
281
+ </div>
282
+ </div>
283
+
284
+ <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border">
285
+ <div className="flex items-center gap-2">
286
+ <Wallet className="h-4 w-4 text-primary" />
287
+ <div className="flex flex-col">
288
+ <span className="text-sm text-muted-foreground">
289
+ Platform Fee
290
+ </span>
291
+ <span className="font-medium">
292
+ {formatCurrency(
293
+ Number(platformFeeAmount.toFixed(2)),
294
+ selectedEscrow.trustline?.name
295
+ )}
296
+ </span>
297
+ </div>
298
+ </div>
299
+ <div className="text-sm text-muted-foreground">
300
+ {selectedEscrow.platformFee}%
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg border">
306
+ <div className="flex items-center gap-2">
307
+ <Wallet className="h-4 w-4 text-primary" />
308
+ <div className="flex flex-col">
309
+ <span className="text-sm text-muted-foreground">
310
+ Trustless Work
311
+ </span>
312
+ <span className="font-medium">
313
+ {formatCurrency(
314
+ Number(trustlessWorkAmount.toFixed(2)),
315
+ selectedEscrow.trustline?.name
316
+ )}
317
+ </span>
318
+ </div>
319
+ </div>
320
+ <div className="text-sm text-muted-foreground">0.3%</div>
321
+ </div>
322
+ </div>
323
+ </Card>
324
+ </div>
325
+ )}
326
+ </div>
327
+ </div>
328
+ );
329
+ };
@@ -0,0 +1,254 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { Button } from "__UI_BASE__/button";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "__UI_BASE__/card";
5
+ import {
6
+ FileCheck2,
7
+ Eye,
8
+ CircleAlert,
9
+ CircleCheckBig,
10
+ Handshake,
11
+ CheckCheck,
12
+ Layers,
13
+ } from "lucide-react";
14
+ import {
15
+ GetEscrowsFromIndexerResponse as Escrow,
16
+ Role,
17
+ } from "@trustless-work/escrow/types";
18
+ import {
19
+ MultiReleaseMilestone,
20
+ SingleReleaseMilestone,
21
+ } from "@trustless-work/escrow";
22
+ import { Badge } from "__UI_BASE__/badge";
23
+ import ApproveMilestoneButton from "../../single-release/approve-milestone/button/ApproveMilestone";
24
+ import ResolveDisputeDialog from "../../single-release/resolve-dispute/dialog/ResolveDispute";
25
+ import DisputeEscrowButton from "../../single-release/dispute-escrow/button/DisputeEscrow";
26
+ import ReleaseEscrowButton from "../../single-release/release-escrow/button/ReleaseEscrow";
27
+ import ChangeMilestoneStatusDialog from "../../single-release/change-milestone-status/dialog/ChangeMilestoneStatus";
28
+ import { formatCurrency } from "@/components/tw-blocks/helpers/format.helper";
29
+
30
+ interface MilestoneCardProps {
31
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone;
32
+ milestoneIndex: number;
33
+ selectedEscrow: Escrow;
34
+ userRolesInEscrow: string[];
35
+ activeRole: Role[];
36
+ onViewDetails: (
37
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone,
38
+ index: number
39
+ ) => void;
40
+ }
41
+
42
+ const MilestoneCardComponent = ({
43
+ milestone,
44
+ milestoneIndex,
45
+ selectedEscrow,
46
+ userRolesInEscrow,
47
+ activeRole,
48
+ onViewDetails,
49
+ }: MilestoneCardProps) => {
50
+ const getMilestoneStatusBadge = (
51
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone
52
+ ) => {
53
+ if ("flags" in milestone && milestone.flags?.disputed) {
54
+ return (
55
+ <Badge variant="destructive">
56
+ <CircleAlert className="h-3.5 w-3.5" />
57
+ <span>Disputed</span>
58
+ </Badge>
59
+ );
60
+ }
61
+ if ("flags" in milestone && milestone.flags?.released) {
62
+ return (
63
+ <Badge variant="default">
64
+ <CircleCheckBig className="h-3.5 w-3.5" />
65
+ <span>Released</span>
66
+ </Badge>
67
+ );
68
+ }
69
+ if (
70
+ "flags" in milestone &&
71
+ milestone.flags?.resolved &&
72
+ !milestone.flags?.disputed
73
+ ) {
74
+ return (
75
+ <Badge variant="default">
76
+ <Handshake className="h-3.5 w-3.5" />
77
+ <span>Resolved</span>
78
+ </Badge>
79
+ );
80
+ }
81
+ if (
82
+ ("flags" in milestone && milestone.flags?.approved) ||
83
+ ("approved" in milestone && milestone.approved)
84
+ ) {
85
+ return (
86
+ <Badge variant="default">
87
+ <CheckCheck className="h-3.5 w-3.5" />
88
+ <span>Approved</span>
89
+ </Badge>
90
+ );
91
+ }
92
+ return (
93
+ <Badge variant="outline">
94
+ <Layers className="h-3.5 w-3.5" />
95
+ <span className="uppercase">
96
+ {milestone.status
97
+ ? milestone.status.match(/[a-z][A-Z]/)
98
+ ? milestone.status.replace(/([A-Z])/g, " $1").toLowerCase()
99
+ : milestone.status.toLowerCase()
100
+ : ""}
101
+ </span>
102
+ </Badge>
103
+ );
104
+ };
105
+
106
+ const getActionButtons = (
107
+ milestone: SingleReleaseMilestone | MultiReleaseMilestone,
108
+ milestoneIndex: number,
109
+ userRolesInEscrow: string[],
110
+ activeRole: Role[]
111
+ ) => {
112
+ const buttons = [] as React.ReactNode[];
113
+ if (
114
+ userRolesInEscrow.includes("serviceProvider") &&
115
+ activeRole.includes("serviceProvider") &&
116
+ milestone.status !== "completed" &&
117
+ !("flags" in milestone && milestone.flags?.approved)
118
+ ) {
119
+ buttons.push(
120
+ <ChangeMilestoneStatusDialog
121
+ key={`change-status-${milestoneIndex}`}
122
+ milestoneIndex={milestoneIndex}
123
+ />
124
+ );
125
+ }
126
+
127
+ if (
128
+ userRolesInEscrow.includes("releaseSigner") &&
129
+ activeRole.includes("releaseSigner") &&
130
+ "flags" in milestone &&
131
+ !milestone.flags?.disputed &&
132
+ milestone.flags?.approved &&
133
+ !milestone.flags?.released
134
+ ) {
135
+ buttons.push(<ReleaseEscrowButton key={`release-${milestoneIndex}`} />);
136
+ }
137
+
138
+ if (
139
+ (userRolesInEscrow.includes("serviceProvider") ||
140
+ userRolesInEscrow.includes("approver")) &&
141
+ (activeRole.includes("serviceProvider") ||
142
+ activeRole.includes("approver")) &&
143
+ "flags" in milestone &&
144
+ !milestone.flags?.disputed &&
145
+ !milestone.flags?.released &&
146
+ !milestone.flags?.resolved
147
+ ) {
148
+ buttons.push(<DisputeEscrowButton key={`dispute-${milestoneIndex}`} />);
149
+ }
150
+
151
+ if (
152
+ userRolesInEscrow.includes("disputeResolver") &&
153
+ activeRole.includes("disputeResolver") &&
154
+ "flags" in milestone &&
155
+ milestone.flags?.disputed
156
+ ) {
157
+ buttons.push(<ResolveDisputeDialog key={`resolve-${milestoneIndex}`} />);
158
+ }
159
+
160
+ if (
161
+ userRolesInEscrow.includes("approver") &&
162
+ activeRole.includes("approver") &&
163
+ (("approved" in milestone && !milestone.approved) ||
164
+ ("flags" in milestone &&
165
+ !milestone.flags?.approved &&
166
+ !milestone.flags?.disputed &&
167
+ !milestone.flags?.released &&
168
+ !milestone.flags?.resolved))
169
+ ) {
170
+ buttons.push(
171
+ <ApproveMilestoneButton
172
+ key={`approve-${milestoneIndex}`}
173
+ milestoneIndex={milestoneIndex}
174
+ />
175
+ );
176
+ }
177
+
178
+ return buttons;
179
+ };
180
+
181
+ return (
182
+ <Card
183
+ key={`milestone-${milestoneIndex}-${milestone.description}-${milestone.status}`}
184
+ className="hover:shadow-lg transition-all duration-200"
185
+ >
186
+ <CardHeader className="pb-4">
187
+ <div className="flex items-center justify-between">
188
+ <CardTitle className="text-base font-semibold text-foreground truncate">
189
+ {milestone.description}
190
+ </CardTitle>
191
+ {getMilestoneStatusBadge(milestone)}
192
+ </div>
193
+ </CardHeader>
194
+
195
+ <CardContent className="pt-0 space-y-4">
196
+ {"amount" in milestone && (
197
+ <div className="flex items-center gap-2 py-2">
198
+ <span className="text-2xl font-bold text-foreground">
199
+ {formatCurrency(milestone.amount, selectedEscrow.trustline?.name)}
200
+ </span>
201
+ </div>
202
+ )}
203
+
204
+ {milestone.evidence && (
205
+ <div className="flex items-center gap-2 p-2 border-primary/20 rounded-lg">
206
+ <FileCheck2 className="w-4 h-4 text-primary flex-shrink-0" />
207
+ <span className="text-xs text-muted-foreground font-medium">
208
+ Evidence provided
209
+ </span>
210
+ </div>
211
+ )}
212
+
213
+ {getActionButtons(
214
+ milestone,
215
+ milestoneIndex,
216
+ userRolesInEscrow,
217
+ activeRole
218
+ )}
219
+
220
+ <Button
221
+ size="sm"
222
+ variant="outline"
223
+ className="w-full border-border text-muted-foreground"
224
+ onClick={(e) => {
225
+ e.stopPropagation();
226
+ onViewDetails(milestone, milestoneIndex);
227
+ }}
228
+ >
229
+ <Eye className="w-3 h-3 mr-2 flex-shrink-0" />
230
+ View Details
231
+ </Button>
232
+ </CardContent>
233
+ </Card>
234
+ );
235
+ };
236
+
237
+ export const MilestoneCard = React.memo(
238
+ MilestoneCardComponent,
239
+ (prev, next) => {
240
+ if (
241
+ prev.milestone === next.milestone &&
242
+ prev.milestoneIndex === next.milestoneIndex &&
243
+ prev.selectedEscrow?.contractId === next.selectedEscrow?.contractId &&
244
+ prev.onViewDetails === next.onViewDetails &&
245
+ prev.activeRole.length === next.activeRole.length &&
246
+ prev.userRolesInEscrow.length === next.userRolesInEscrow.length &&
247
+ prev.activeRole.every((r, i) => r === next.activeRole[i]) &&
248
+ prev.userRolesInEscrow.every((r, i) => r === next.userRolesInEscrow[i])
249
+ ) {
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+ );