@apptimate/ui 3.0.0 → 3.1.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": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -6,9 +6,10 @@ import { Info } from 'lucide-react';
6
6
 
7
7
  interface HintIconProps {
8
8
  text: string;
9
+ icon?: React.ReactNode;
9
10
  }
10
11
 
11
- export function HintIcon({ text }: HintIconProps) {
12
+ export function HintIcon({ text, icon }: HintIconProps) {
12
13
  const [show, setShow] = useState(false);
13
14
  const [pos, setPos] = useState({ top: 0, left: 0, isBelow: false });
14
15
  const iconRef = React.useRef<HTMLSpanElement>(null);
@@ -40,7 +41,7 @@ export function HintIcon({ text }: HintIconProps) {
40
41
  onMouseEnter={handleMouseEnter}
41
42
  onMouseLeave={() => setShow(false)}
42
43
  >
43
- <Info size={14} className="text-foreground-muted hover:text-foreground-1 transition-colors cursor-help" />
44
+ {icon ? icon : <Info size={14} className="text-foreground-muted hover:text-foreground-1 transition-colors cursor-help" />}
44
45
  </span>
45
46
  {show && typeof document !== 'undefined' && ReactDOM.createPortal(
46
47
  <div
@@ -2,7 +2,8 @@
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
4
4
  import { cn } from "@apptimate/core-lib";
5
- import { CreditCard, Banknote, Building, Trash2, Loader2, Split, Check, AlertCircle } from "lucide-react";
5
+ import { CreditCard, Banknote, Building, Trash2, Loader2, Split, Check, AlertCircle, AlertTriangle, CheckCircle } from "lucide-react";
6
+ import { HintIcon } from "../base-components/HintIcon";
6
7
 
7
8
  /* ── Types (mirroring sales POS types) ── */
8
9
 
@@ -61,11 +62,13 @@ export interface PaymentSectionProps {
61
62
  materialTypes?: any[];
62
63
  /** Special mode codes that are allowed (e.g. 'credit-gold') */
63
64
  allowedSpecialModes?: string[];
65
+ /** Fetcher for cheque leaves (for auto-complete) */
66
+ fetchChequeLeaves?: (query: string) => Promise<{ is_success: boolean; result?: any[] }>;
64
67
  }
65
68
 
66
69
  /* ── Component ── */
67
70
 
68
- export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [] }: PaymentSectionProps) {
71
+ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves }: PaymentSectionProps) {
69
72
  const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
70
73
  const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
71
74
  const [isLoading, setIsLoading] = useState(false);
@@ -241,7 +244,7 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
241
244
  {/* ── Single Mode: Entry with fields (no delete) ── */}
242
245
  {selectionMode === "single" && payments.length === 1 && (
243
246
  <PaymentEntryRow entry={payments[0]} mode={paymentModes.find((m) => m.id === payments[0].mode_id)}
244
- bankAccounts={bankAccounts} materialTypes={materialTypes} onUpdate={(updates) => updatePayment(0, updates)} onRemove={() => {}} showRemove={false} />
247
+ bankAccounts={bankAccounts} materialTypes={materialTypes} onUpdate={(updates) => updatePayment(0, updates)} onRemove={() => {}} showRemove={false} fetchChequeLeaves={fetchChequeLeaves} />
245
248
  )}
246
249
 
247
250
  {/* ── Split Mode: Multi-entry UI ── */}
@@ -251,7 +254,7 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
251
254
  const mode = paymentModes.find((m) => m.id === entry.mode_id);
252
255
  return (
253
256
  <PaymentEntryRow key={idx} entry={entry} mode={mode} bankAccounts={bankAccounts} materialTypes={materialTypes}
254
- onUpdate={(updates) => updatePayment(idx, updates)} onRemove={() => removePayment(idx)} />
257
+ onUpdate={(updates) => updatePayment(idx, updates)} onRemove={() => removePayment(idx)} fetchChequeLeaves={fetchChequeLeaves} />
255
258
  );
256
259
  })}
257
260
  {/* Add split mode buttons */}
@@ -325,9 +328,10 @@ interface PaymentEntryRowProps {
325
328
  onUpdate: (updates: Partial<PaymentEntry>) => void;
326
329
  onRemove: () => void;
327
330
  showRemove?: boolean;
331
+ fetchChequeLeaves?: (query: string) => Promise<{ is_success: boolean; result?: any[] }>;
328
332
  }
329
333
 
330
- function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpdate, onRemove, showRemove = true }: PaymentEntryRowProps) {
334
+ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpdate, onRemove, showRemove = true, fetchChequeLeaves }: PaymentEntryRowProps) {
331
335
  const fields = mode?.field_config?.fields || [];
332
336
 
333
337
  const getModeIcon = () => {
@@ -369,9 +373,24 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
369
373
  <div className="grid grid-cols-2 gap-2.5">
370
374
  {fields.map((field: PaymentModeField) => (
371
375
  <div key={field.key} className="flex flex-col gap-1">
372
- <label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider">
376
+ <label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider flex items-center">
373
377
  {field.label}
374
378
  {field.required && <span className="text-danger-alt ml-0.5">*</span>}
379
+ {field.key === "cheque_number" && fetchChequeLeaves && !!entry.metadata[field.key] && (
380
+ <span className="ml-1.5 flex items-center">
381
+ {entry.metadata.cheque_leaf_id ? (
382
+ <HintIcon
383
+ text="Selected from your cheque book. This will be automatically issued."
384
+ icon={<CheckCircle size={13} className="text-green-500" />}
385
+ />
386
+ ) : (
387
+ <HintIcon
388
+ text="Not selected from cheque book. This cheque will not be auto-issued."
389
+ icon={<AlertTriangle size={13} className="text-amber-500" />}
390
+ />
391
+ )}
392
+ </span>
393
+ )}
375
394
  </label>
376
395
  {field.type === "bank_account_select" ? (
377
396
  <select value={entry.metadata[field.key] || ""}
@@ -389,6 +408,21 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
389
408
  <option value="">Select Material...</option>
390
409
  {materialTypes.map((m) => (<option key={m.id} value={m.id}>{m.name}</option>))}
391
410
  </select>
411
+ ) : field.key === "cheque_number" && fetchChequeLeaves ? (
412
+ <ChequeAutocomplete
413
+ value={String(entry.metadata[field.key] || "")}
414
+ onChange={(val, leafId, suggestion) => {
415
+ const newMetadata = { ...entry.metadata, [field.key]: val, cheque_leaf_id: leafId || "" };
416
+ if (suggestion?.cheque_book?.bank_account) {
417
+ const ba = suggestion.cheque_book.bank_account;
418
+ if (ba.bank_name) newMetadata.bank_name = ba.bank_name;
419
+ if (ba.branch) newMetadata.branch = ba.branch;
420
+ }
421
+ onUpdate({ metadata: newMetadata });
422
+ }}
423
+ fetchChequeLeaves={fetchChequeLeaves}
424
+ readonly={field.readonly}
425
+ />
392
426
  ) : (
393
427
  <input type={field.type === "number" ? "number" : field.type === "date" ? "date" : "text"}
394
428
  {...(field.type === "number" ? { step: "any" } : {})}
@@ -404,3 +438,109 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
404
438
  </div>
405
439
  );
406
440
  }
441
+
442
+ /* ── ChequeAutocomplete ── */
443
+
444
+ function ChequeAutocomplete({
445
+ value,
446
+ onChange,
447
+ fetchChequeLeaves,
448
+ readonly
449
+ }: {
450
+ value: string;
451
+ onChange: (val: string, leafId?: number, suggestion?: any) => void;
452
+ fetchChequeLeaves: (q: string) => Promise<{ is_success: boolean; result?: any[] }>;
453
+ readonly?: boolean;
454
+ }) {
455
+ const [query, setQuery] = useState(value);
456
+ const [suggestions, setSuggestions] = useState<any[]>([]);
457
+ const [isOpen, setIsOpen] = useState(false);
458
+ const [isLoading, setIsLoading] = useState(false);
459
+ const wrapperRef = useRef<HTMLDivElement>(null);
460
+
461
+ useEffect(() => {
462
+ setQuery(value);
463
+ }, [value]);
464
+
465
+ useEffect(() => {
466
+ const handleClickOutside = (e: MouseEvent) => {
467
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
468
+ setIsOpen(false);
469
+ }
470
+ };
471
+ document.addEventListener("mousedown", handleClickOutside);
472
+ return () => document.removeEventListener("mousedown", handleClickOutside);
473
+ }, []);
474
+
475
+ const fetchSuggestions = useCallback(async (search: string) => {
476
+ setIsLoading(true);
477
+ try {
478
+ const res = await fetchChequeLeaves(search);
479
+ if (res.is_success && res.result) {
480
+ setSuggestions(res.result);
481
+ setIsOpen(true);
482
+ }
483
+ } catch (e) {
484
+ console.error(e);
485
+ } finally {
486
+ setIsLoading(false);
487
+ }
488
+ }, [fetchChequeLeaves]);
489
+
490
+ useEffect(() => {
491
+ if (!isOpen) return;
492
+ const timer = setTimeout(() => {
493
+ fetchSuggestions(query);
494
+ }, 300);
495
+ return () => clearTimeout(timer);
496
+ }, [query, isOpen, fetchSuggestions]);
497
+
498
+ return (
499
+ <div ref={wrapperRef} className="relative">
500
+ <input
501
+ type="text"
502
+ value={query}
503
+ onChange={(e) => {
504
+ setQuery(e.target.value);
505
+ onChange(e.target.value, undefined, undefined); // reset ID when typed manually
506
+ setIsOpen(true);
507
+ }}
508
+ onFocus={() => {
509
+ if (!readonly) {
510
+ setIsOpen(true);
511
+ fetchSuggestions(query);
512
+ }
513
+ }}
514
+ readOnly={readonly}
515
+ className={cn("w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-2.5 py-2.5 text-[12px] text-foreground-1 outline-none focus:border-primary/40 transition-all", readonly && "opacity-60 bg-surface-0 cursor-not-allowed border-border")}
516
+ placeholder="Type to search cheques..."
517
+ />
518
+ {isLoading && (
519
+ <div className="absolute right-2 top-2.5">
520
+ <Loader2 size={14} className="animate-spin text-foreground-subtle" />
521
+ </div>
522
+ )}
523
+ {isOpen && suggestions.length > 0 && !readonly && (
524
+ <div className="absolute z-50 mt-1 w-full bg-surface-0 border border-border-subtle rounded-[8px] shadow-lg max-h-48 overflow-y-auto">
525
+ {suggestions.map((s) => (
526
+ <div
527
+ key={s.id}
528
+ className="px-3 py-2 text-[12px] text-foreground-1 hover:bg-primary/5 hover:text-primary cursor-pointer transition-colors"
529
+ onClick={() => {
530
+ setQuery(s.cheque_number);
531
+ onChange(s.cheque_number, s.id, s);
532
+ setIsOpen(false);
533
+ }}
534
+ >
535
+ <div className="font-semibold">{s.cheque_number}</div>
536
+ <div className="text-[10px] text-foreground-subtle flex items-center gap-2">
537
+ {s.cheque_book?.bank_account?.bank_name && <span>{s.cheque_book.bank_account.bank_name}</span>}
538
+ {s.amount && <span>Amount: {s.amount}</span>}
539
+ </div>
540
+ </div>
541
+ ))}
542
+ </div>
543
+ )}
544
+ </div>
545
+ );
546
+ }