@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -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
- const d = new Date(date);
12
-
13
- // Format to "2026-05-07 4:35PM"
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
- <Button
30
- variant="soft"
31
- color="default"
32
- className={cn("!p-0 text-gray-500", triggerClassName)}
33
- icon={<Search size={18} strokeWidth={2} />}
34
- ariaLabel="Search"
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 totalPaid = useMemo(() => payments.reduce((s, p) => s + p.amount, 0), [payments]);
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) => m.is_active && !payments.some(p => p.mode_id === m.id))
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 Due" : balance < -0.01 ? "Change Due" : "Settled"}
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>