@apptimate/ui 7.1.0 → 7.2.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": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -56,6 +56,8 @@ export interface PaymentSectionProps {
56
56
  onPaymentsChange: (payments: PaymentEntry[]) => void;
57
57
  /** Optional: show compact variant */
58
58
  compact?: boolean;
59
+ /** When true, initially show only Cash/Card with a 'More Options' toggle for other modes (used in POS) */
60
+ collapseModes?: boolean;
59
61
  /** Fetcher for payment modes */
60
62
  fetchPaymentModes: () => Promise<{ is_success: boolean; result?: any }>;
61
63
  /** Fetcher for bank accounts */
@@ -72,11 +74,12 @@ export interface PaymentSectionProps {
72
74
 
73
75
  /* ── Component ── */
74
76
 
75
- export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves, variant = "default" }: PaymentSectionProps) {
77
+ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, collapseModes, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves, variant = "default" }: PaymentSectionProps) {
76
78
  const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
77
79
  const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
78
80
  const [isLoading, setIsLoading] = useState(false);
79
81
  const [selectionMode, setSelectionMode] = useState<"single" | "split">("single");
82
+ const [showMoreModes, setShowMoreModes] = useState(false);
80
83
  const paymentsRef = useRef(payments);
81
84
 
82
85
  // Keep ref in sync
@@ -214,36 +217,74 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
214
217
  return (
215
218
  <div className="space-y-3">
216
219
  {/* ── Mode Selection Buttons ── */}
217
- <div className="flex flex-wrap gap-2">
218
- {paymentModes
219
- .filter((m) => m.is_active)
220
- .map((mode) => {
221
- const isActive = selectionMode === "single" && payments.length === 1 && payments[0].mode_id === mode.id;
222
- return (
223
- <button key={mode.id} type="button" onClick={() => selectSingleMode(mode)}
220
+ {(() => {
221
+ const activeModes = paymentModes.filter((m) => m.is_active);
222
+ const primaryModes = activeModes.filter((m) => ["cash", "card"].includes(m.code));
223
+ const secondaryModes = activeModes.filter((m) => !["cash", "card"].includes(m.code));
224
+ // If a secondary mode is currently selected, auto-expand
225
+ const isSecondaryActive = selectionMode === "single" && payments.length === 1 && secondaryModes.some(m => m.id === payments[0].mode_id);
226
+ const shouldShowMore = !collapseModes || showMoreModes || isSecondaryActive || selectionMode === "split";
227
+
228
+ return (
229
+ <div className="flex flex-wrap gap-2">
230
+ {primaryModes.map((mode) => {
231
+ const isActive = selectionMode === "single" && payments.length === 1 && payments[0].mode_id === mode.id;
232
+ return (
233
+ <button key={mode.id} type="button" onClick={() => selectSingleMode(mode)}
234
+ className={cn(
235
+ "inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
236
+ isActive
237
+ ? "bg-primary text-primary-foreground border-primary shadow-sm"
238
+ : "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
239
+ )}>
240
+ {getModeIcon(mode.code)}
241
+ {mode.name}
242
+ </button>
243
+ );
244
+ })}
245
+
246
+ {/* More Options toggle */}
247
+ {secondaryModes.length > 0 && !shouldShowMore && (
248
+ <button type="button" onClick={() => setShowMoreModes(true)}
249
+ className="inline-flex items-center gap-1.5 px-3.5 py-2.5 text-[12px] font-semibold rounded-[10px] border border-dashed border-border-subtle text-foreground-subtle hover:border-primary/40 hover:text-primary hover:bg-primary/5 transition-all">
250
+ <span className="text-[14px]">···</span>
251
+ More Options
252
+ </button>
253
+ )}
254
+
255
+ {/* Secondary modes (shown when expanded) */}
256
+ {shouldShowMore && secondaryModes.map((mode) => {
257
+ const isActive = selectionMode === "single" && payments.length === 1 && payments[0].mode_id === mode.id;
258
+ return (
259
+ <button key={mode.id} type="button" onClick={() => selectSingleMode(mode)}
260
+ className={cn(
261
+ "inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
262
+ isActive
263
+ ? "bg-primary text-primary-foreground border-primary shadow-sm"
264
+ : "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
265
+ )}>
266
+ {getModeIcon(mode.code)}
267
+ {mode.name}
268
+ </button>
269
+ );
270
+ })}
271
+
272
+ {/* Split Button (shown when expanded) */}
273
+ {shouldShowMore && (
274
+ <button type="button" onClick={activateSplitMode}
224
275
  className={cn(
225
276
  "inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
226
- isActive
277
+ selectionMode === "split"
227
278
  ? "bg-primary text-primary-foreground border-primary shadow-sm"
228
279
  : "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
229
280
  )}>
230
- {getModeIcon(mode.code)}
231
- {mode.name}
281
+ <Split size={14} />
282
+ Multiple Methods
232
283
  </button>
233
- );
234
- })}
235
- {/* Split Button */}
236
- <button type="button" onClick={activateSplitMode}
237
- className={cn(
238
- "inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
239
- selectionMode === "split"
240
- ? "bg-primary text-primary-foreground border-primary shadow-sm"
241
- : "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
242
- )}>
243
- <Split size={14} />
244
- Multiple Methods
245
- </button>
246
- </div>
284
+ )}
285
+ </div>
286
+ );
287
+ })()}
247
288
 
248
289
  {/* ── Single Mode: Entry with fields (no delete) ── */}
249
290
  {selectionMode === "single" && payments.length === 1 && (
@@ -36,12 +36,13 @@ interface MetadataPanelProps {
36
36
  isSubmitting: boolean;
37
37
  renderExtraMeta?: () => React.ReactNode;
38
38
  fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
39
+ isFullscreen?: boolean;
39
40
  }
40
41
 
41
42
  export function MetadataPanel({
42
43
  config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, expectedDeliveryDate, notes, payments,
43
44
  onPartyChange, onWarehouseChange, onDateChange, onExpectedDeliveryDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
44
- renderExtraMeta, fetchChequeLeaves,
45
+ renderExtraMeta, fetchChequeLeaves, isFullscreen,
45
46
  }: MetadataPanelProps) {
46
47
  const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
47
48
  const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
@@ -132,7 +133,9 @@ export function MetadataPanel({
132
133
  );
133
134
 
134
135
  return (
135
- <div className="flex flex-col h-full gap-6">
136
+ <div className="flex flex-col h-full">
137
+ {/* ── Scrollable Content ── */}
138
+ <div className="flex-1 overflow-y-auto custom-scrollbar space-y-6 pr-1">
136
139
  {/* ── Extra Module-Specific Meta (e.g. adjustment type/reason) ── */}
137
140
  {renderExtraMeta && renderExtraMeta()}
138
141
 
@@ -216,8 +219,8 @@ export function MetadataPanel({
216
219
  </Section>
217
220
  )}
218
221
 
219
- {/* ── Totals ── */}
220
- {config.showPrices && lineItems.length > 0 && (
222
+ {/* ── Totals (hidden for POS — shown at bottom of left panel instead) ── */}
223
+ {config.showPrices && config.type !== 'pos' && lineItems.length > 0 && (
221
224
  <div className="bg-surface-0 rounded-[12px] border border-border-subtle p-4 space-y-2">
222
225
  <TotalRow label="Subtotal" value={formatCurrency(subtotal)} />
223
226
  {discountTotal > 0 && (
@@ -239,23 +242,29 @@ export function MetadataPanel({
239
242
  fetchBankAccounts={getBankAccountsLookup}
240
243
  fetchChequeLeaves={fetchChequeLeaves}
241
244
  compact={true}
245
+ collapseModes={config.type === 'pos'}
242
246
  />
243
247
  </Section>
244
248
  )}
249
+ </div>
245
250
 
246
- {/* ── Spacer ── */}
247
- <div className="flex-1" />
248
-
249
- {/* ── Submit Button ── */}
250
- <Button
251
- type="submit"
252
- color="primary"
253
- isDisabled={lineItems.length === 0}
254
- isLoading={isSubmitting}
255
- className="!w-full !py-4 !text-[14px] !font-semibold !rounded-[10px]"
256
- >
257
- {config.submitLabel}
258
- </Button>
251
+ {/* ── Submit Button (sticky bottom) ── */}
252
+ <div className="shrink-0 pt-4">
253
+ <Button
254
+ type="submit"
255
+ color="primary"
256
+ isDisabled={lineItems.length === 0}
257
+ isLoading={isSubmitting}
258
+ className={cn(
259
+ "!w-full !font-semibold transition-all",
260
+ isFullscreen
261
+ ? "!h-[72px] !text-[18px] !rounded-[12px] shadow-lg shadow-primary/20"
262
+ : "!py-4 !text-[14px] !rounded-[10px]"
263
+ )}
264
+ >
265
+ {config.submitLabel}
266
+ </Button>
267
+ </div>
259
268
  </div>
260
269
  );
261
270
  }
@@ -353,7 +353,7 @@ export function ProductSelectionPanel({
353
353
  };
354
354
 
355
355
  return (
356
- <div className="flex flex-col h-full">
356
+ <div className="flex flex-col flex-1 min-h-0 w-full">
357
357
  {/* ── Search Bar ── */}
358
358
  {!config.disableItemSelection && (
359
359
  <div ref={panelRef} className="relative mb-4">
@@ -31,6 +31,7 @@ export function ProductTransactionScreen({
31
31
  fetchChequeLeaves,
32
32
  initialData,
33
33
  onAddNewItem,
34
+ isFullscreen,
34
35
  }: ProductTransactionScreenProps) {
35
36
  // ── State ──
36
37
  const [lineItems, setLineItems] = useState<TransactionLineItem[]>([]);
@@ -503,7 +504,7 @@ export function ProductTransactionScreen({
503
504
  {/* ── Two-Panel Layout ── */}
504
505
  <div className="flex-1 flex gap-5 min-h-0">
505
506
  {/* Left: Product Selection (75%) */}
506
- <div className="flex-[3] min-w-0 flex flex-col">
507
+ <div className="flex-[3] min-w-0 flex flex-col min-h-0">
507
508
  <ProductSelectionPanel
508
509
  config={config}
509
510
  partyId={partyId}
@@ -515,13 +516,41 @@ export function ProductTransactionScreen({
515
516
  onToggleExpand={handleToggleExpand}
516
517
  onAddNewItem={onAddNewItem}
517
518
  />
519
+
520
+ {/* ── POS: Amount to Pay Summary Bar ── */}
521
+ {config.type === 'pos' && lineItems.length > 0 && (
522
+ <div className="shrink-0 mt-3 bg-surface-0 rounded-[14px] border border-border-subtle px-5 py-3.5 flex items-center justify-between gap-6">
523
+ <div className="flex items-center gap-6">
524
+ <div className="flex items-center gap-2">
525
+ <span className="text-[11.5px] font-semibold text-foreground-subtle uppercase tracking-wide">Subtotal</span>
526
+ <span className="text-[13.5px] font-bold text-foreground-1">
527
+ {subtotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
528
+ </span>
529
+ </div>
530
+ {discountTotal > 0 && (
531
+ <div className="flex items-center gap-2">
532
+ <span className="text-[11.5px] font-semibold text-foreground-subtle uppercase tracking-wide">Discount</span>
533
+ <span className="text-[13.5px] font-bold text-green-600">
534
+ -{discountTotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
535
+ </span>
536
+ </div>
537
+ )}
538
+ </div>
539
+ <div className="flex items-center gap-3">
540
+ <span className="text-[12px] font-bold text-foreground-subtle uppercase tracking-wide">Amount to Pay</span>
541
+ <span className="text-[22px] font-extrabold text-foreground-0 tracking-tight tabular-nums">
542
+ {grandTotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
543
+ </span>
544
+ </div>
545
+ </div>
546
+ )}
518
547
  </div>
519
548
 
520
549
  {/* Divider */}
521
550
  <div className="w-px bg-border-subtle/60 shrink-0" />
522
551
 
523
552
  {/* Right: Metadata (25%) */}
524
- <div className="flex-1 min-w-0 flex flex-col overflow-y-auto custom-scrollbar pr-1">
553
+ <div className="flex-1 min-w-0 flex flex-col">
525
554
  <MetadataPanel
526
555
  config={config}
527
556
  lineItems={lineItems}
@@ -617,6 +646,7 @@ export function ProductTransactionScreen({
617
646
  isSubmitting={submitting}
618
647
  renderExtraMeta={renderExtraMeta}
619
648
  fetchChequeLeaves={fetchChequeLeaves}
649
+ isFullscreen={isFullscreen}
620
650
  />
621
651
  </div>
622
652
  </div>
@@ -319,6 +319,8 @@ export interface ProductTransactionScreenProps {
319
319
  initialData?: any;
320
320
  /** Optional callback to create a new item directly from the search bar */
321
321
  onAddNewItem?: (callbacks: { setItemSearch: (value: string) => void }) => void;
322
+ /** Whether the screen is currently in fullscreen mode */
323
+ isFullscreen?: boolean;
322
324
  }
323
325
 
324
326
  export interface TransactionPayload {
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Modal, ModalFooter, Button, Badge, DateLabel } from "../index";
5
+
6
+ interface DirectTransactionDetailModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ transaction: any | null;
10
+ }
11
+
12
+ export default function DirectTransactionDetailModal({
13
+ isOpen,
14
+ onClose,
15
+ transaction,
16
+ }: DirectTransactionDetailModalProps) {
17
+ if (!isOpen || !transaction) return null;
18
+
19
+ const formatCurrency = (val: any) => {
20
+ return Number(val).toLocaleString("en-US", {
21
+ minimumFractionDigits: 2,
22
+ maximumFractionDigits: 2,
23
+ });
24
+ };
25
+
26
+ const API_URL = process.env.NEXT_PUBLIC_API_URL;
27
+
28
+ return (
29
+ <Modal isOpen={isOpen} onClose={onClose} title="Transaction Details" size="md">
30
+ <div className="space-y-6 text-sm">
31
+ <div className="grid grid-cols-2 gap-y-4 gap-x-6 border-b border-gray-100 pb-5">
32
+ <div className="space-y-1">
33
+ <p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
34
+ Type
35
+ </p>
36
+ <div>
37
+ <Badge
38
+ color={transaction.type === "income" ? "success" : "danger"}
39
+ variant="flat"
40
+ >
41
+ {transaction.type === "income" ? "Income" : "Expense"}
42
+ </Badge>
43
+ </div>
44
+ </div>
45
+ <div className="space-y-1">
46
+ <p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
47
+ Date
48
+ </p>
49
+ <div className="font-medium text-gray-900">
50
+ <DateLabel date={transaction.transaction_date} />
51
+ </div>
52
+ </div>
53
+ <div className="space-y-1">
54
+ <p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
55
+ Reference
56
+ </p>
57
+ <p className="font-medium text-gray-900">
58
+ {transaction.reference || "-"}
59
+ </p>
60
+ </div>
61
+ <div className="space-y-1">
62
+ <p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
63
+ Bank Account
64
+ </p>
65
+ <p className="font-medium text-gray-900">
66
+ {transaction.payment_method === "cash"
67
+ ? "Cash"
68
+ : transaction.bank_account?.account_name || "-"}
69
+ </p>
70
+ </div>
71
+ <div className="col-span-2 space-y-1">
72
+ <p className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
73
+ Description
74
+ </p>
75
+ <p className="font-medium text-gray-900 whitespace-pre-wrap">
76
+ {transaction.description || "-"}
77
+ </p>
78
+ </div>
79
+ </div>
80
+
81
+ <div className="flex items-center justify-between pt-2 pb-5 border-b border-gray-100">
82
+ <p className="text-gray-500 font-medium">Total Amount</p>
83
+ <p className="text-xl font-bold text-gray-900">
84
+ {formatCurrency(transaction.amount)}
85
+ </p>
86
+ </div>
87
+
88
+ {/* Attachments Section */}
89
+ {transaction.attachments && transaction.attachments.length > 0 && (
90
+ <div className="space-y-3 pt-2">
91
+ <h3 className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">
92
+ Attachments ({transaction.attachments.length})
93
+ </h3>
94
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
95
+ {transaction.attachments.map((attachment: any, idx: number) => {
96
+ const url = typeof attachment === 'string'
97
+ ? (attachment.startsWith('http') ? attachment : `${API_URL}/storage/${attachment}`)
98
+ : attachment.url;
99
+
100
+ const isImage = url?.match(/\.(jpeg|jpg|gif|png|webp)$/i) != null ||
101
+ (typeof attachment === 'object' && attachment.mime_type?.startsWith('image/'));
102
+
103
+ return (
104
+ <a
105
+ key={idx}
106
+ href={url}
107
+ target="_blank"
108
+ rel="noopener noreferrer"
109
+ className="block group relative rounded-lg border border-gray-200 overflow-hidden hover:border-primary/50 transition-colors bg-gray-50"
110
+ >
111
+ {isImage ? (
112
+ <div className="aspect-square w-full">
113
+ {/* eslint-disable-next-line @next/next/no-img-element */}
114
+ <img
115
+ src={url}
116
+ alt="Attachment"
117
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
118
+ />
119
+ </div>
120
+ ) : (
121
+ <div className="aspect-square w-full flex flex-col items-center justify-center p-3 text-center">
122
+ <div className="w-10 h-10 rounded-full bg-white shadow-sm flex items-center justify-center mb-2 text-gray-400">
123
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
124
+ </div>
125
+ <span className="text-[10px] font-medium text-gray-600 truncate w-full" title={attachment.original_name || attachment.filename || `File ${idx + 1}`}>
126
+ {attachment.original_name || attachment.filename || `File ${idx + 1}`}
127
+ </span>
128
+ </div>
129
+ )}
130
+ </a>
131
+ );
132
+ })}
133
+ </div>
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ <ModalFooter>
139
+ <Button variant="flat" color="secondary" onClick={onClose}>
140
+ Close
141
+ </Button>
142
+ </ModalFooter>
143
+ </Modal>
144
+ );
145
+ }
package/src/index.tsx CHANGED
@@ -80,3 +80,4 @@ export * from "./base-components/ChartOfAccountPicker";
80
80
  export * from './common-components/print/TemplateRenderer';
81
81
  export { default as TransactionPrintSelector } from './common-components/print/TransactionPrintSelector';
82
82
  export { default as DirectExpenseModal } from "./finance-components/DirectExpenseModal";
83
+ export { default as DirectTransactionDetailModal } from "./finance-components/DirectTransactionDetailModal";