@apptimate/ui 7.2.0 → 7.3.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 +1 -1
- package/src/base-components/FormattedNumberInput.tsx +118 -0
- package/src/base-components/Select.tsx +1 -1
- package/src/common-components/DashboardLayout.tsx +101 -57
- package/src/common-components/PaymentSection.tsx +134 -130
- package/src/common-components/item-wizard/ItemFormWizard.tsx +118 -60
- package/src/common-components/pickers/PartyPicker.tsx +28 -401
- package/src/common-components/pickers/QuickPartyAddModal.tsx +414 -0
- package/src/common-components/pickers/UomPicker.tsx +20 -20
- package/src/common-components/pickers/modals/BatchSelectionModal.tsx +167 -114
- package/src/common-components/transaction/ProductSelectionPanel.tsx +6 -4
- package/src/common-components/transaction/ProductTransactionScreen.tsx +3 -3
- package/src/common-components/transaction/types.ts +2 -0
- package/src/components/shared/ImageUploadComponent.tsx +3 -0
- package/src/finance-components/DirectTransactionDetailModal.tsx +9 -1
- package/src/index.tsx +2 -0
|
@@ -31,7 +31,7 @@ export interface ItemFormData {
|
|
|
31
31
|
min_stock_level: string; max_stock_level: string;
|
|
32
32
|
reorder_point: string; reorder_qty: string; lead_time_days: string;
|
|
33
33
|
is_available_purchase: boolean; is_available_sell: boolean; is_available_ecommerce: boolean;
|
|
34
|
-
has_stocks: boolean; is_taxable: boolean; is_returnable: boolean; is_discountable: boolean;
|
|
34
|
+
has_stocks: boolean; is_taxable: boolean; is_returnable: boolean; is_discountable: boolean; allow_phantom_bom: boolean;
|
|
35
35
|
weight: string; weight_unit: string; dimensions: string;
|
|
36
36
|
attributes: { name: string; values: string[] }[];
|
|
37
37
|
// Page 3: Variants
|
|
@@ -56,7 +56,7 @@ export const emptyFormData: ItemFormData = {
|
|
|
56
56
|
min_stock_level: "", max_stock_level: "",
|
|
57
57
|
reorder_point: "", reorder_qty: "", lead_time_days: "",
|
|
58
58
|
is_available_purchase: true, is_available_sell: true, is_available_ecommerce: false,
|
|
59
|
-
has_stocks: true, is_taxable: true, is_returnable: true, is_discountable: true,
|
|
59
|
+
has_stocks: true, is_taxable: true, is_returnable: true, is_discountable: true, allow_phantom_bom: false,
|
|
60
60
|
weight: "", weight_unit: "", dimensions: "",
|
|
61
61
|
attributes: [{ name: "", values: [] }],
|
|
62
62
|
variants: [],
|
|
@@ -314,8 +314,8 @@ export default function ItemFormWizard({
|
|
|
314
314
|
setFormData(p => ({ ...p, attributes: newAttrs }));
|
|
315
315
|
}}
|
|
316
316
|
className={`p-2.5 rounded-lg transition-colors mt-0.5 ${isEditing
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
? "text-gray-300 cursor-not-allowed"
|
|
318
|
+
: "text-gray-400 hover:text-danger-alt hover:bg-danger-50"
|
|
319
319
|
}`}
|
|
320
320
|
>
|
|
321
321
|
<Trash size={16} />
|
|
@@ -336,6 +336,7 @@ export default function ItemFormWizard({
|
|
|
336
336
|
label={<span className="flex items-center gap-1.5">Available for Purchase <HintIcon text="Can be purchased from suppliers" /></span>}
|
|
337
337
|
checked={formData.is_available_purchase}
|
|
338
338
|
onChange={(e: any) => setFormData((p) => ({ ...p, is_available_purchase: e.target.checked }))}
|
|
339
|
+
disabled={formData.allow_phantom_bom}
|
|
339
340
|
/>
|
|
340
341
|
<Checkbox
|
|
341
342
|
label={<span className="flex items-center gap-1.5">Available for Sale <HintIcon text="Can be sold to customers" /></span>}
|
|
@@ -351,66 +352,88 @@ export default function ItemFormWizard({
|
|
|
351
352
|
label={<span className="flex items-center gap-1.5">Track Stock <HintIcon text="Monitor inventory levels" /></span>}
|
|
352
353
|
checked={formData.has_stocks}
|
|
353
354
|
onChange={(e: any) => setFormData((p) => ({ ...p, has_stocks: e.target.checked }))}
|
|
354
|
-
disabled={isEditing && formData.can_edit_variants === false}
|
|
355
|
+
disabled={formData.allow_phantom_bom || (isEditing && formData.can_edit_variants === false)}
|
|
355
356
|
/>
|
|
356
357
|
<Checkbox
|
|
357
358
|
label={<span className="flex items-center gap-1.5">Taxable <HintIcon text="Subject to taxes" /></span>}
|
|
358
359
|
checked={formData.is_taxable}
|
|
359
360
|
onChange={(e: any) => setFormData((p) => ({ ...p, is_taxable: e.target.checked }))}
|
|
361
|
+
disabled={formData.allow_phantom_bom}
|
|
360
362
|
/>
|
|
361
363
|
<Checkbox
|
|
362
364
|
label={<span className="flex items-center gap-1.5">Returnable <HintIcon text="Eligible for returns" /></span>}
|
|
363
365
|
checked={formData.is_returnable}
|
|
364
366
|
onChange={(e: any) => setFormData((p) => ({ ...p, is_returnable: e.target.checked }))}
|
|
367
|
+
disabled={formData.allow_phantom_bom}
|
|
365
368
|
/>
|
|
366
369
|
<Checkbox
|
|
367
370
|
label={<span className="flex items-center gap-1.5">Discountable <HintIcon text="Eligible for discounts" /></span>}
|
|
368
371
|
checked={formData.is_discountable}
|
|
369
372
|
onChange={(e: any) => setFormData((p) => ({ ...p, is_discountable: e.target.checked }))}
|
|
373
|
+
disabled={formData.allow_phantom_bom}
|
|
374
|
+
/>
|
|
375
|
+
<Checkbox
|
|
376
|
+
label={<span className="flex items-center gap-1.5">Allow Phantom BOM <HintIcon text="Can be used as a finished good in a Phantom BOM" /></span>}
|
|
377
|
+
checked={formData.allow_phantom_bom}
|
|
378
|
+
onChange={(e: any) => setFormData((p) => {
|
|
379
|
+
const checked = e.target.checked;
|
|
380
|
+
if (checked) {
|
|
381
|
+
return {
|
|
382
|
+
...p,
|
|
383
|
+
allow_phantom_bom: true,
|
|
384
|
+
is_available_purchase: false,
|
|
385
|
+
has_stocks: false,
|
|
386
|
+
is_taxable: false,
|
|
387
|
+
is_returnable: false,
|
|
388
|
+
is_discountable: false,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return { ...p, allow_phantom_bom: false };
|
|
392
|
+
})}
|
|
370
393
|
/>
|
|
371
394
|
</div>
|
|
372
395
|
</div>
|
|
373
396
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
</div>
|
|
411
|
-
|
|
397
|
+
<div className="p-4 bg-gray-50/50 rounded-xl border border-gray-100">
|
|
398
|
+
<p className="text-[11px] font-bold text-gray-400 uppercase tracking-wider mb-3">Tracking Options</p>
|
|
399
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6">
|
|
400
|
+
<label className="flex items-center gap-2 text-sm text-[#2D3142] cursor-pointer">
|
|
401
|
+
<input
|
|
402
|
+
type="radio"
|
|
403
|
+
name="tracking_type"
|
|
404
|
+
className="w-4 h-4 text-[#7A52F4] border-gray-300 focus:ring-[#7A52F4] disabled:opacity-50"
|
|
405
|
+
checked={formData.tracking_type === 'none'}
|
|
406
|
+
onChange={() => setFormData(p => ({ ...p, tracking_type: 'none' }))}
|
|
407
|
+
disabled={isEditing}
|
|
408
|
+
/>
|
|
409
|
+
<span>None</span>
|
|
410
|
+
</label>
|
|
411
|
+
<label className="flex items-center gap-2 text-sm text-[#2D3142] cursor-pointer">
|
|
412
|
+
<input
|
|
413
|
+
type="radio"
|
|
414
|
+
name="tracking_type"
|
|
415
|
+
className="w-4 h-4 text-[#7A52F4] border-gray-300 focus:ring-[#7A52F4] disabled:opacity-50"
|
|
416
|
+
checked={formData.tracking_type === 'batch'}
|
|
417
|
+
onChange={() => setFormData(p => ({ ...p, tracking_type: 'batch' }))}
|
|
418
|
+
disabled={isEditing}
|
|
419
|
+
/>
|
|
420
|
+
<span className="flex items-center gap-1.5">Batches <HintIcon text="Track items by manufacturing batch or lot" /></span>
|
|
421
|
+
</label>
|
|
422
|
+
<label className="flex items-center gap-2 text-sm text-[#2D3142] cursor-pointer">
|
|
423
|
+
<input
|
|
424
|
+
type="radio"
|
|
425
|
+
name="tracking_type"
|
|
426
|
+
className="w-4 h-4 text-[#7A52F4] border-gray-300 focus:ring-[#7A52F4] disabled:opacity-50"
|
|
427
|
+
checked={formData.tracking_type === 'serial'}
|
|
428
|
+
onChange={() => setFormData(p => ({ ...p, tracking_type: 'serial' }))}
|
|
429
|
+
disabled={isEditing}
|
|
430
|
+
/>
|
|
431
|
+
<span className="flex items-center gap-1.5">Serial Numbers <HintIcon text="Track each individual unit with a unique serial number" /></span>
|
|
432
|
+
</label>
|
|
412
433
|
</div>
|
|
413
434
|
|
|
435
|
+
</div>
|
|
436
|
+
|
|
414
437
|
{formData.has_stocks && formData.tracking_type !== 'serial' && (
|
|
415
438
|
<div className="p-4 bg-gray-50/50 rounded-xl border border-gray-100">
|
|
416
439
|
<p className="text-[11px] font-bold text-gray-400 uppercase tracking-wider mb-3">Valuation Method (COGS)</p>
|
|
@@ -507,7 +530,10 @@ export default function ItemFormWizard({
|
|
|
507
530
|
|
|
508
531
|
const generateCombinations = useCallback(() => {
|
|
509
532
|
const validAttributes = formData.attributes.filter(a => a.name && a.values.length > 0);
|
|
510
|
-
if (validAttributes.length === 0)
|
|
533
|
+
if (validAttributes.length === 0) {
|
|
534
|
+
setFormData(p => p.variants.length > 0 ? { ...p, variants: [] } : p);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
511
537
|
|
|
512
538
|
let combinations: any[] = [{}];
|
|
513
539
|
validAttributes.forEach((attr) => {
|
|
@@ -523,28 +549,60 @@ export default function ItemFormWizard({
|
|
|
523
549
|
combinations = newCombinations;
|
|
524
550
|
});
|
|
525
551
|
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
552
|
+
setFormData(p => {
|
|
553
|
+
const newVariants: VariantRow[] = combinations.map((combo) => {
|
|
554
|
+
const variantName = Object.values(combo).join(" - ");
|
|
555
|
+
|
|
556
|
+
const existingVariant = p.variants.find((v) => {
|
|
557
|
+
// Primary match: exact attribute_values match
|
|
558
|
+
if (v.attribute_values && Object.keys(v.attribute_values).length > 0) {
|
|
559
|
+
const comboKeys = Object.keys(combo);
|
|
560
|
+
const vKeys = Object.keys(v.attribute_values);
|
|
561
|
+
if (comboKeys.length === vKeys.length && comboKeys.every(k => v.attribute_values[k] === combo[k])) {
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Fallback: match by variant_name
|
|
566
|
+
if (v.variant_name && v.variant_name === variantName) return true;
|
|
567
|
+
return false;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
if (existingVariant) {
|
|
571
|
+
return { ...existingVariant, variant_name: variantName, attribute_values: combo };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
variant_name: variantName,
|
|
576
|
+
sku: "",
|
|
577
|
+
barcode: "",
|
|
578
|
+
sale_price: p.sale_price || "0",
|
|
579
|
+
cost_price: p.cost_price || "0",
|
|
580
|
+
min_sales_price: p.min_sales_price || "0",
|
|
581
|
+
attribute_values: combo,
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
if (p.variants.length !== newVariants.length) {
|
|
586
|
+
return { ...p, variants: newVariants };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const hasChanges = newVariants.some((v, idx) => v !== p.variants[idx]);
|
|
590
|
+
if (hasChanges) {
|
|
591
|
+
return { ...p, variants: newVariants };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return p;
|
|
537
595
|
});
|
|
596
|
+
}, [formData.attributes, setFormData]);
|
|
538
597
|
|
|
539
|
-
|
|
540
|
-
}, [formData.attributes, formData.sale_price, formData.cost_price, setFormData]);
|
|
598
|
+
const attributesStr = JSON.stringify(formData.attributes);
|
|
541
599
|
|
|
542
|
-
// Auto-generate variants on step 2
|
|
600
|
+
// Auto-generate variants on step 2
|
|
543
601
|
React.useEffect(() => {
|
|
544
|
-
if (step === 2 && formData.has_variants
|
|
602
|
+
if (step === 2 && formData.has_variants) {
|
|
545
603
|
generateCombinations();
|
|
546
604
|
}
|
|
547
|
-
}, [step, formData.has_variants,
|
|
605
|
+
}, [step, formData.has_variants, attributesStr, generateCombinations]);
|
|
548
606
|
|
|
549
607
|
const renderPage3 = () => (
|
|
550
608
|
<div className="space-y-5 animate-fade-in">
|