@apptimate/ui 1.9.0 → 2.0.0
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.
package/package.json
CHANGED
|
@@ -8,12 +8,39 @@ export interface DateLabelProps {
|
|
|
8
8
|
export function DateLabel({ date, className = '' }: DateLabelProps) {
|
|
9
9
|
if (!date) return <span className="text-gray-300">-</span>;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
let isDateOnly = false;
|
|
12
|
+
let parsedDate: string | Date = date;
|
|
13
|
+
|
|
14
|
+
if (typeof date === 'string') {
|
|
15
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
16
|
+
isDateOnly = true;
|
|
17
|
+
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
|
|
18
|
+
parsedDate = date.replace(' ', 'T') + 'Z';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let d: Date;
|
|
23
|
+
if (isDateOnly && typeof date === 'string') {
|
|
24
|
+
const [y, m, day] = date.split('-');
|
|
25
|
+
d = new Date(parseInt(y, 10), parseInt(m, 10) - 1, parseInt(day, 10));
|
|
26
|
+
} else {
|
|
27
|
+
d = new Date(parsedDate);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isNaN(d.getTime())) return <span className="text-gray-300">-</span>;
|
|
31
|
+
|
|
14
32
|
const year = d.getFullYear();
|
|
15
33
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
16
34
|
const day = String(d.getDate()).padStart(2, '0');
|
|
35
|
+
|
|
36
|
+
if (isDateOnly) {
|
|
37
|
+
return (
|
|
38
|
+
<div className={`flex flex-col ${className}`}>
|
|
39
|
+
<span className="text-gray-600 font-medium whitespace-nowrap">{year}-{month}-{day}</span>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
let hours = d.getHours();
|
|
18
45
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
19
46
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Search } from 'lucide-react';
|
|
2
|
+
import { Search, X } from 'lucide-react';
|
|
3
3
|
import { Dropdown } from './Dropdown';
|
|
4
4
|
import { Input } from './Input';
|
|
5
5
|
import { Button } from './Button';
|
|
@@ -26,16 +26,33 @@ export const DesktopSearchPopover = ({
|
|
|
26
26
|
align="right"
|
|
27
27
|
contentClassName="w-[300px] p-2"
|
|
28
28
|
trigger={
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
searchValue ? (
|
|
30
|
+
<div className="flex items-center gap-1.5 bg-primary-50 hover:bg-primary-100 transition-colors pl-3 pr-1.5 py-1.5 rounded-full cursor-pointer border border-primary-100">
|
|
31
|
+
<Search size={14} className="text-primary-500" />
|
|
32
|
+
<span className="text-xs font-bold text-primary-700 max-w-[120px] truncate">{searchValue}</span>
|
|
33
|
+
<div
|
|
34
|
+
className="p-1 hover:bg-primary-200 rounded-full text-primary-600 transition-colors"
|
|
35
|
+
onClick={(e) => {
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
onSearchChange("");
|
|
38
|
+
}}
|
|
39
|
+
title="Clear search"
|
|
40
|
+
>
|
|
41
|
+
<X size={14} />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
) : (
|
|
45
|
+
<Button
|
|
46
|
+
variant="soft"
|
|
47
|
+
color="default"
|
|
48
|
+
className={cn("!p-0 text-gray-500", triggerClassName)}
|
|
49
|
+
icon={<Search size={18} strokeWidth={2} />}
|
|
50
|
+
ariaLabel="Search"
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
36
53
|
}
|
|
37
54
|
>
|
|
38
|
-
<div className="p-1">
|
|
55
|
+
<div className="p-1" onClick={(e) => e.stopPropagation()}>
|
|
39
56
|
<Input
|
|
40
57
|
autoFocus
|
|
41
58
|
placeholder={placeholder}
|
|
@@ -135,7 +135,7 @@ export function DashboardLayout({
|
|
|
135
135
|
size: 22,
|
|
136
136
|
className: isActive ? "stroke-[2.5]" : "stroke-[2]"
|
|
137
137
|
})}
|
|
138
|
-
<span className={`text-[11px] ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
|
|
138
|
+
<span className={`text-[11px] text-center leading-tight px-1 ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
|
|
139
139
|
</div>
|
|
140
140
|
);
|
|
141
141
|
})}
|
|
@@ -92,7 +92,9 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
92
92
|
}, []);
|
|
93
93
|
|
|
94
94
|
// ── Calculations ──
|
|
95
|
-
const
|
|
95
|
+
const isCreditPayment = useCallback((payment: PaymentEntry) => payment.mode_code === "credit", []);
|
|
96
|
+
const totalPaid = useMemo(() => payments.reduce((s, p) => s + (isCreditPayment(p) ? 0 : p.amount), 0), [payments, isCreditPayment]);
|
|
97
|
+
const creditAmount = useMemo(() => payments.reduce((s, p) => s + (isCreditPayment(p) ? p.amount : 0), 0), [payments, isCreditPayment]);
|
|
96
98
|
const balance = useMemo(() => totalAmount - totalPaid, [totalAmount, totalPaid]);
|
|
97
99
|
const formatCurrency = (v: number) => v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
98
100
|
|
|
@@ -118,8 +120,34 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
118
120
|
const updatePayment = useCallback((index: number, updates: Partial<PaymentEntry>) => {
|
|
119
121
|
const next = [...payments];
|
|
120
122
|
next[index] = { ...next[index], ...updates };
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
selectionMode === "split" &&
|
|
126
|
+
Object.prototype.hasOwnProperty.call(updates, "amount") &&
|
|
127
|
+
!isCreditPayment(next[index]) &&
|
|
128
|
+
next.length > 1
|
|
129
|
+
) {
|
|
130
|
+
const targetIndex = next.findIndex((payment, paymentIndex) =>
|
|
131
|
+
paymentIndex !== index &&
|
|
132
|
+
!isCreditPayment(payment) &&
|
|
133
|
+
Number(payment.amount || 0) <= 0
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (targetIndex >= 0) {
|
|
137
|
+
const paidExceptTarget = next.reduce((sum, payment, paymentIndex) => {
|
|
138
|
+
if (paymentIndex === targetIndex || isCreditPayment(payment)) return sum;
|
|
139
|
+
return sum + Number(payment.amount || 0);
|
|
140
|
+
}, 0);
|
|
141
|
+
|
|
142
|
+
next[targetIndex] = {
|
|
143
|
+
...next[targetIndex],
|
|
144
|
+
amount: Math.max(0, Math.round((totalAmount - paidExceptTarget) * 100) / 100),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
onPaymentsChange(next);
|
|
122
|
-
}, [payments, onPaymentsChange]);
|
|
150
|
+
}, [payments, selectionMode, totalAmount, isCreditPayment, onPaymentsChange]);
|
|
123
151
|
|
|
124
152
|
// ── Auto-sync single mode amount when totalAmount changes ──
|
|
125
153
|
useEffect(() => {
|
|
@@ -207,7 +235,11 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
207
235
|
{/* Add split mode buttons */}
|
|
208
236
|
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
209
237
|
{paymentModes
|
|
210
|
-
.filter((m) =>
|
|
238
|
+
.filter((m) => {
|
|
239
|
+
if (!m.is_active) return false;
|
|
240
|
+
const canBeMultiple = ["card", "bank_transfer", "cheque"].includes(m.code);
|
|
241
|
+
return canBeMultiple || !payments.some(p => p.mode_id === m.id);
|
|
242
|
+
})
|
|
211
243
|
.map((mode) => (
|
|
212
244
|
<button key={mode.id} onClick={() => addPayment(mode)}
|
|
213
245
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-semibold rounded-[8px] bg-surface-0 border border-border-subtle text-foreground-subtle hover:border-primary/40 hover:text-primary transition-all">
|
|
@@ -225,8 +257,11 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
225
257
|
<TotalRow label="Amount to Pay" value={formatCurrency(totalAmount)} bold />
|
|
226
258
|
<div className="border-t border-border-subtle my-2" />
|
|
227
259
|
<TotalRow label="Total Paid" value={formatCurrency(totalPaid)} />
|
|
260
|
+
{creditAmount > 0.01 && (
|
|
261
|
+
<TotalRow label="Credit Amount" value={formatCurrency(creditAmount)} className="text-amber-600" />
|
|
262
|
+
)}
|
|
228
263
|
<TotalRow
|
|
229
|
-
label={balance > 0.01 ? "Balance
|
|
264
|
+
label={balance > 0.01 ? "Outstanding Balance" : balance < -0.01 ? "Change Due" : "Settled"}
|
|
230
265
|
value={formatCurrency(Math.abs(balance))}
|
|
231
266
|
className={balance > 0.01 ? "text-danger-alt" : "text-green-600"}
|
|
232
267
|
bold
|
|
@@ -236,7 +271,7 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
236
271
|
<AlertCircle size={12} /><span>Change: {formatCurrency(Math.abs(balance))}</span>
|
|
237
272
|
</div>
|
|
238
273
|
)}
|
|
239
|
-
{Math.abs(balance) < 0.01 && (
|
|
274
|
+
{Math.abs(balance) < 0.01 && creditAmount <= 0.01 && (
|
|
240
275
|
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-green-600 font-medium">
|
|
241
276
|
<Check size={12} /><span>Fully paid</span>
|
|
242
277
|
</div>
|