@apptimate/ui 4.7.0 → 4.8.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": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState } from 'react';
4
- import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Building2, ChevronRight, Check } from 'lucide-react';
4
+ import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Building2, ChevronRight, Check, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
5
5
  import { Dropdown, DropdownItem } from '../base-components/Dropdown';
6
6
 
7
7
  import Link from 'next/link';
@@ -67,6 +67,7 @@ export function DashboardLayout({
67
67
  }) {
68
68
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
69
69
  const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
70
+ const [isSubSidebarOpen, setIsSubSidebarOpen] = useState(true);
70
71
  const pathname = usePathname() || "";
71
72
  const router = useRouter();
72
73
 
@@ -140,11 +141,22 @@ export function DashboardLayout({
140
141
  return (
141
142
  <div className="flex h-screen bg-[#F4F5F7] overflow-hidden font-sans print:h-auto print:overflow-visible print:bg-white">
142
143
  {/* Desktop Global Sidebar */}
143
- <aside className="hidden md:flex print:hidden w-[80px] bg-white border-r border-gray-200 flex-col items-center py-6 shrink-0 z-20">
144
+ <aside className="hidden md:flex print:hidden w-[80px] bg-white border-r border-gray-200 flex-col items-center py-6 shrink-0 z-30 relative group">
144
145
  <div className="w-12 h-12 flex items-center justify-center mb-8">
145
146
  {logo}
146
147
  </div>
147
148
 
149
+ {/* Floating Open Button when Sub-sidebar is closed */}
150
+ {!isSubSidebarOpen && activeMenu && activeMenu.groups.length > 0 && (
151
+ <button
152
+ onClick={() => setIsSubSidebarOpen(true)}
153
+ className="hidden md:flex absolute -right-4 top-6 z-40 w-8 h-8 bg-white border border-gray-200 rounded-full shadow-sm items-center justify-center text-gray-500 hover:text-[#2D3142] hover:bg-gray-50 transition-all outline-none opacity-0 group-hover:opacity-100 cursor-pointer"
154
+ title="Expand Sidebar"
155
+ >
156
+ <PanelLeftOpen size={16} />
157
+ </button>
158
+ )}
159
+
148
160
  <style dangerouslySetInnerHTML={{
149
161
  __html: `
150
162
  .sidebar-scroll {
@@ -179,7 +191,10 @@ export function DashboardLayout({
179
191
  return (
180
192
  <div
181
193
  key={menu.id}
182
- onClick={() => setActiveMenuId(menu.id)}
194
+ onClick={() => {
195
+ setActiveMenuId(menu.id);
196
+ setIsSubSidebarOpen(true);
197
+ }}
183
198
  className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors w-full px-1 ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
184
199
  }`}
185
200
  >
@@ -226,8 +241,17 @@ export function DashboardLayout({
226
241
 
227
242
  {/* Desktop Secondary Sidebar */}
228
243
  {activeMenu && activeMenu.groups.length > 0 && (
229
- <aside className="hidden md:flex print:hidden w-[210px] bg-white border-gray-200 flex-col py-8 shrink-0 z-10">
230
- <h2 className="px-6 text-xl font-bold text-[#2D3142]/80 mb-6">{activeMenu.label}</h2>
244
+ <>
245
+ <aside className={cn(
246
+ "hidden md:flex print:hidden bg-white border-gray-200 flex-col py-8 shrink-0 z-10 transition-all duration-300 overflow-hidden group",
247
+ isSubSidebarOpen ? "w-[210px]" : "w-0 opacity-0 px-0"
248
+ )}>
249
+ <div className="flex items-center justify-between px-6 mb-6 w-[210px]">
250
+ <h2 className="text-xl font-bold text-[#2D3142]/80 truncate">{activeMenu.label}</h2>
251
+ <button onClick={() => setIsSubSidebarOpen(false)} className="text-gray-400 hover:text-[#2D3142] transition-colors flex-shrink-0 outline-none opacity-0 group-hover:opacity-100 cursor-pointer">
252
+ <PanelLeftClose size={18} />
253
+ </button>
254
+ </div>
231
255
  <div className="flex-1 overflow-y-auto px-4 space-y-6">
232
256
  {activeMenu.groups.map((group) => (
233
257
  <div key={group.id}>
@@ -296,6 +320,8 @@ export function DashboardLayout({
296
320
  </div>
297
321
  )}
298
322
  </aside>
323
+
324
+ </>
299
325
  )}
300
326
 
301
327
  {/* Main Content Area & Mobile Header */}
@@ -57,24 +57,24 @@ export function ProductTransactionScreen({
57
57
  setLineItems(prev => prev.map(l => {
58
58
  if (l.is_free_qty || !l.variant_id || !evaluatedRules[l.variant_id]) return l;
59
59
  const rules = evaluatedRules[l.variant_id];
60
-
60
+
61
61
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
62
62
  const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
63
63
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
64
64
  const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
65
65
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
66
66
  ? Number(batchPrice)
67
- : (actualVariant
68
- ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
67
+ : (actualVariant
68
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
69
69
  : Number(l.item[config.priceField] || 0));
70
-
70
+
71
71
  const uoms = l.item.uom?.uom_group?.uoms || [];
72
72
  const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
73
73
  const factor = Number(currentUom?.conversion_factor) || 1;
74
74
  const effectiveQty = l.quantity * factor;
75
75
 
76
76
  const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
77
-
77
+
78
78
  return {
79
79
  ...l,
80
80
  pricing_rules: rules,
@@ -111,6 +111,13 @@ export function ProductTransactionScreen({
111
111
  setWarehouseId(initialData.warehouse_id);
112
112
  if (initialData.warehouse?.name) setWarehouseName(initialData.warehouse.name);
113
113
  }
114
+ if (initialData.transaction_date || initialData.order_date || initialData.quotation_date || initialData.date) {
115
+ setTransactionDate(initialData.transaction_date || initialData.order_date || initialData.quotation_date || initialData.date);
116
+ }
117
+ if (initialData.expected_delivery_date || initialData.expected_delivery) {
118
+ setExpectedDeliveryDate(initialData.expected_delivery_date || initialData.expected_delivery);
119
+ }
120
+
114
121
  if (initialData.lines && Array.isArray(initialData.lines)) {
115
122
  setLineItems(
116
123
  initialData.lines.map((l: any) => {
@@ -135,7 +142,7 @@ export function ProductTransactionScreen({
135
142
  };
136
143
  })
137
144
  );
138
-
145
+
139
146
  // Populate pricing rules for existing items if party is selected
140
147
  if (initialData.party_id && ['sales', 'pos', 'quotation'].includes(config.type)) {
141
148
  evaluatePriceLists({
@@ -152,7 +159,7 @@ export function ProductTransactionScreen({
152
159
  return line;
153
160
  }));
154
161
  }
155
- }).catch(() => {});
162
+ }).catch(() => { });
156
163
  }
157
164
  }
158
165
  }
@@ -186,7 +193,7 @@ export function ProductTransactionScreen({
186
193
  ? Number(batchPrice)
187
194
  : (actualVariant ? Number(actualVariant[variantPriceField] || itemWithVariants[config.priceField] || 0) : Number(itemWithVariants[config.priceField] || 0));
188
195
  const qty = extra?.quantity || 1;
189
-
196
+
190
197
  const uomId = itemWithVariants.uom_id;
191
198
  const uoms = itemWithVariants.uom?.uom_group?.uoms || [];
192
199
  const currentUom = uoms.find((u: any) => u.id === uomId);
@@ -206,7 +213,7 @@ export function ProductTransactionScreen({
206
213
  discountAmt = priceDetails.discountAmount * factor;
207
214
  appliedRule = priceDetails.appliedRule;
208
215
  }
209
-
216
+
210
217
  const newLine: TransactionLineItem = {
211
218
  uid: `${itemWithVariants.id}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
212
219
  item: itemWithVariants,
@@ -253,14 +260,14 @@ export function ProductTransactionScreen({
253
260
  const batchPrice = ['sales', 'quotation'].includes(config.type) && updatedLine.batches?.[0] ? (updatedLine.batches[0] as any)[batchPriceField] : undefined;
254
261
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
255
262
  ? Number(batchPrice)
256
- : (actualVariant
257
- ? Number(actualVariant[variantPriceField] || updatedLine.item[config.priceField] || 0)
263
+ : (actualVariant
264
+ ? Number(actualVariant[variantPriceField] || updatedLine.item[config.priceField] || 0)
258
265
  : Number(updatedLine.item[config.priceField] || 0));
259
-
266
+
260
267
  const uoms = updatedLine.item.uom?.uom_group?.uoms || [];
261
268
  const currentUom = uoms.find((u: any) => u.id === (updatedLine.uom_id || updatedLine.item.uom_id));
262
269
  const factor = Number(currentUom?.conversion_factor) || 1;
263
-
270
+
264
271
  const effectiveQty = updatedLine.quantity * factor;
265
272
 
266
273
  console.log("DEBUG: handleUpdateLine rules -> ", {
@@ -275,7 +282,7 @@ export function ProductTransactionScreen({
275
282
  updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
276
283
  } else if (isQtyChanged || isUomChanged || isPriceChanged || isDiscountChanged) {
277
284
  updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
278
-
285
+
279
286
  const isManualPriceOverride = isPriceChanged && !isUomChanged;
280
287
  if (isManualPriceOverride) {
281
288
  updatedLine.applied_price_rule = undefined;
@@ -286,6 +293,14 @@ export function ProductTransactionScreen({
286
293
  updatedLine.batches[0].quantity = updatedLine.quantity;
287
294
  }
288
295
 
296
+ if (updatedLine.is_free_qty) {
297
+ updatedLine.unit_price = 0;
298
+ updatedLine.discount_amount = 0;
299
+ updatedLine.discount_percent = 0;
300
+ updatedLine.line_total = 0;
301
+ updatedLine.applied_price_rule = undefined;
302
+ }
303
+
289
304
  return updatedLine;
290
305
  })
291
306
  );
@@ -470,7 +485,7 @@ export function ProductTransactionScreen({
470
485
  expectedDeliveryDate={expectedDeliveryDate}
471
486
  notes={notes}
472
487
  payments={payments}
473
- onPartyChange={async (id, party) => {
488
+ onPartyChange={async (id, party) => {
474
489
  if (partyId !== id && lineItems.length > 0 && ['sales', 'pos', 'quotation'].includes(config.type)) {
475
490
  if (hasActivePriceLists) {
476
491
  toast.loading('Checking price changes...', { id: 'price-calc' });
@@ -480,12 +495,12 @@ export function ProductTransactionScreen({
480
495
  transaction_type: config.type,
481
496
  items: lineItems.filter(l => l.variant_id && !l.is_free_qty).map(l => ({ variant_id: l.variant_id! }))
482
497
  });
483
-
498
+
484
499
  toast.dismiss('price-calc');
485
-
500
+
486
501
  if (res.is_success && res.result) {
487
502
  const evaluatedRules = res.result as Record<number, any[]>;
488
-
503
+
489
504
  let hasActualPriceChanges = false;
490
505
  for (const l of lineItems) {
491
506
  if (!l.is_free_qty && l.variant_id && evaluatedRules[l.variant_id]) {
@@ -496,24 +511,24 @@ export function ProductTransactionScreen({
496
511
  const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
497
512
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
498
513
  ? Number(batchPrice)
499
- : (actualVariant
500
- ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
514
+ : (actualVariant
515
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
501
516
  : Number(l.item[config.priceField] || 0));
502
-
517
+
503
518
  const uoms = l.item.uom?.uom_group?.uoms || [];
504
519
  const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
505
520
  const factor = Number(currentUom?.conversion_factor) || 1;
506
521
  const effectiveQty = l.quantity * factor;
507
522
 
508
523
  const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
509
-
524
+
510
525
  if (Math.abs((priceDetails.unitPrice * factor) - l.unit_price) > 0.001) {
511
526
  hasActualPriceChanges = true;
512
527
  break;
513
528
  }
514
529
  }
515
530
  }
516
-
531
+
517
532
  if (hasActualPriceChanges) {
518
533
  const previousRulesApplied = lineItems.some(l => l.applied_price_rule);
519
534
  setPriceEvaluationModal({
@@ -542,8 +557,8 @@ export function ProductTransactionScreen({
542
557
  }
543
558
  }
544
559
  }
545
- setPartyId(id);
546
- setPartyName(party?.name ?? null);
560
+ setPartyId(id);
561
+ setPartyName(party?.name ?? null);
547
562
  }}
548
563
  onWarehouseChange={(id, name) => { setWarehouseId(id); setWarehouseName(name ?? null); }}
549
564
  onDateChange={setTransactionDate}
@@ -574,15 +589,15 @@ export function ProductTransactionScreen({
574
589
  size="sm"
575
590
  >
576
591
  <div className="text-[14px] text-foreground-1 leading-relaxed">
577
- {priceEvaluationModal.type === 'confirm'
592
+ {priceEvaluationModal.type === 'confirm'
578
593
  ? "A few items have price changes based on the price list. Do you want to apply them?"
579
594
  : "Prices need to be changed based on the party selection."}
580
595
  </div>
581
596
  <ModalFooter>
582
597
  {priceEvaluationModal.type === 'confirm' && (
583
- <Button
584
- variant="flat"
585
- color="secondary"
598
+ <Button
599
+ variant="flat"
600
+ color="secondary"
586
601
  onClick={() => {
587
602
  setPartyId(priceEvaluationModal.pendingPartyId);
588
603
  setPartyName(priceEvaluationModal.pendingParty?.name ?? null);
@@ -592,8 +607,8 @@ export function ProductTransactionScreen({
592
607
  No
593
608
  </Button>
594
609
  )}
595
- <Button
596
- color="primary"
610
+ <Button
611
+ color="primary"
597
612
  onClick={() => {
598
613
  applyEvaluatedPrices(priceEvaluationModal.evaluatedRules);
599
614
  setPartyId(priceEvaluationModal.pendingPartyId);
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Button, Input, ModalFooter } from "../index";
5
+ import { PaymentSection, PaymentEntry } from "../common-components/PaymentSection";
6
+ import toast from "react-hot-toast";
7
+
8
+ interface PaymentFormProps {
9
+ invoice: any;
10
+ onSuccess: () => void;
11
+ onCancel: () => void;
12
+ onRecordPayment: (payload: any) => Promise<{ is_success: boolean; message?: string }>;
13
+ fetchPaymentModes: () => Promise<any>;
14
+ fetchBankAccounts: () => Promise<any>;
15
+ fetchChequeLeaves?: (query: string, page?: number) => Promise<any>;
16
+ }
17
+
18
+ export function PaymentForm({
19
+ invoice,
20
+ onSuccess,
21
+ onCancel,
22
+ onRecordPayment,
23
+ fetchPaymentModes,
24
+ fetchBankAccounts,
25
+ fetchChequeLeaves,
26
+ }: PaymentFormProps) {
27
+ const [isSubmitting, setIsSubmitting] = useState(false);
28
+ const outstanding = Math.abs(Number(invoice.amount_due || 0) - Number(invoice.amount_settled || invoice.amount_received || 0));
29
+
30
+ const [paymentDate, setPaymentDate] = useState(new Date().toISOString().split("T")[0]);
31
+ const [reference, setReference] = useState("");
32
+ const [remarks, setRemarks] = useState("");
33
+ const [payments, setPayments] = useState<PaymentEntry[]>([]);
34
+
35
+ const handleSubmit = async (e: React.FormEvent) => {
36
+ e.preventDefault();
37
+ if (payments.length === 0) {
38
+ toast.error("Please add at least one payment");
39
+ return;
40
+ }
41
+
42
+ const totalToPay = payments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);
43
+ if (totalToPay > outstanding + 0.01) {
44
+ toast.error(`Payment amount exceeds outstanding balance (${outstanding.toFixed(2)})`);
45
+ return;
46
+ }
47
+
48
+ setIsSubmitting(true);
49
+ let successCount = 0;
50
+
51
+ try {
52
+ for (const p of payments) {
53
+ if (Number(p.amount) <= 0) continue;
54
+ const payload: any = {
55
+ invoice_id: invoice.id,
56
+ payment_date: paymentDate,
57
+ payment_amount: p.amount,
58
+ payment_method: p.mode_code,
59
+ payment_mode_id: p.mode_id,
60
+ remarks: remarks || null,
61
+ reference: reference || null,
62
+ };
63
+
64
+ if (p.metadata) {
65
+ if (p.metadata.reference) payload.reference = p.metadata.reference;
66
+ if (p.metadata.bank_account_id) payload.bank_account_id = p.metadata.bank_account_id;
67
+ payload.mode_metadata = p.metadata;
68
+ }
69
+
70
+ const res = await onRecordPayment(payload);
71
+ if (res.is_success) successCount++;
72
+ else toast.error(`Failed for ${p.mode_name}: ${res.message}`);
73
+ }
74
+
75
+ if (successCount > 0) {
76
+ toast.success("Payment recorded successfully");
77
+ onSuccess();
78
+ }
79
+ } catch (e: any) {
80
+ toast.error(e.message || "Error recording payment");
81
+ } finally {
82
+ setIsSubmitting(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <form onSubmit={handleSubmit} className="space-y-4">
88
+ <div className="flex flex-col gap-1.5">
89
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Payment Date *</label>
90
+ <Input
91
+ type="date"
92
+ value={paymentDate}
93
+ onChange={(e) => setPaymentDate(e.target.value)}
94
+ />
95
+ </div>
96
+
97
+ <PaymentSection
98
+ totalAmount={outstanding}
99
+ payments={payments}
100
+ onPaymentsChange={setPayments}
101
+ fetchPaymentModes={async () => {
102
+ const res = await fetchPaymentModes();
103
+ if (res.is_success && Array.isArray(res.result)) {
104
+ res.result = res.result.filter((m: any) => {
105
+ const code = m.code?.toLowerCase();
106
+ return code !== "credit" && code !== "credit-metal";
107
+ });
108
+ }
109
+ return res;
110
+ }}
111
+ fetchBankAccounts={fetchBankAccounts}
112
+ fetchChequeLeaves={fetchChequeLeaves}
113
+ />
114
+
115
+ <div className="flex flex-col gap-4 mt-2">
116
+ <div className="flex flex-col gap-1.5">
117
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Reference No</label>
118
+ <Input
119
+ value={reference}
120
+ onChange={(e) => setReference(e.target.value)}
121
+ placeholder="Leave blank to auto-generate"
122
+ />
123
+ </div>
124
+ <div className="flex flex-col gap-1.5">
125
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Remarks</label>
126
+ <textarea
127
+ value={remarks}
128
+ onChange={(e) => setRemarks(e.target.value)}
129
+ rows={2}
130
+ placeholder="Optional notes..."
131
+ className="w-full bg-white border-[1.5px] border-gray-200 rounded-[10px] px-3.5 py-2.5 text-[13px] text-gray-800 outline-none transition-all hover:border-gray-300 focus:border-primary/50 resize-none"
132
+ />
133
+ </div>
134
+ </div>
135
+
136
+ <ModalFooter>
137
+ <Button type="button" variant="flat" color="default" onClick={onCancel}>Cancel</Button>
138
+ <Button type="submit" color="primary" isLoading={isSubmitting}>Record Payment</Button>
139
+ </ModalFooter>
140
+ </form>
141
+ );
142
+ }
package/src/index.tsx CHANGED
@@ -56,3 +56,4 @@ export * from './common-components/item-wizard/SkuConfigModal';
56
56
 
57
57
  // Transaction
58
58
  export * from './common-components/transaction';
59
+ export * from './finance-components/PaymentForm';