@apptimate/ui 3.5.0 → 3.7.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.
Files changed (25) hide show
  1. package/package.json +3 -2
  2. package/src/base-components/Input.tsx +6 -3
  3. package/src/base-components/TagsInput.tsx +106 -0
  4. package/src/base-components/WizardModal.tsx +162 -0
  5. package/src/common-components/item-wizard/ItemFormWizard.tsx +705 -0
  6. package/src/common-components/item-wizard/SkuConfigModal.tsx +314 -0
  7. package/src/common-components/pickers/BrandPicker.tsx +194 -0
  8. package/src/common-components/pickers/CategoryPicker.tsx +342 -0
  9. package/src/common-components/pickers/EntityPickerModal.tsx +221 -0
  10. package/src/common-components/pickers/PartyPicker.tsx +231 -0
  11. package/src/common-components/pickers/TransactionItemPicker.tsx +166 -0
  12. package/src/common-components/pickers/UomGroupPicker.tsx +196 -0
  13. package/src/common-components/pickers/UomPicker.tsx +306 -0
  14. package/src/common-components/pickers/WarehousePicker.tsx +135 -0
  15. package/src/common-components/pickers/index.ts +11 -0
  16. package/src/common-components/pickers/modals/BatchSelectionModal.tsx +359 -0
  17. package/src/common-components/pickers/modals/SerialSelectionModal.tsx +247 -0
  18. package/src/common-components/pickers/modals/types.ts +30 -0
  19. package/src/common-components/transaction/MetadataPanel.tsx +417 -0
  20. package/src/common-components/transaction/ProductSelectionPanel.tsx +587 -0
  21. package/src/common-components/transaction/ProductTransactionScreen.tsx +250 -0
  22. package/src/common-components/transaction/index.ts +17 -0
  23. package/src/common-components/transaction/types.ts +300 -0
  24. package/src/components/shared/ImageUploadComponent.tsx +1 -2
  25. package/src/index.tsx +20 -0
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ import React, { useState, useCallback } from "react";
4
+ import { Button, Input, Modal, ModalFooter, Select } from "@apptimate/ui";
5
+ import { Plus, Ruler } from "lucide-react";
6
+ import {
7
+ EntityPickerModal,
8
+ PickerItem,
9
+ PickerTrigger,
10
+ } from "./EntityPickerModal";
11
+ import { getUomGroups, createUom } from "@apptimate/core-lib";
12
+ import { UomGroupPicker } from "./UomGroupPicker";
13
+ import toast from "react-hot-toast";
14
+
15
+ interface UomPickerProps {
16
+ value?: number | string | null;
17
+ displayValue?: string | null;
18
+ onChange: (uom: { id: number; name: string; abbreviation: string; uom_group_id?: number } | null) => void;
19
+ label?: string;
20
+ placeholder?: string;
21
+ isRequired?: boolean;
22
+ excludeId?: number | string | null;
23
+ /** If provided, restricts the picker to only show UOMs from this specific group ID */
24
+ uomGroupId?: number | null;
25
+ pickerZIndex?: number;
26
+ baseUnitsOnly?: boolean;
27
+ }
28
+
29
+ export function UomPicker({
30
+ value,
31
+ displayValue,
32
+ onChange,
33
+ label = "Unit of Measure",
34
+ placeholder = "Select UOM…",
35
+ isRequired = false,
36
+ excludeId,
37
+ uomGroupId,
38
+ pickerZIndex,
39
+ baseUnitsOnly = false,
40
+ }: UomPickerProps) {
41
+ const [isOpen, setIsOpen] = useState(false);
42
+ const [search, setSearch] = useState("");
43
+ const [groups, setGroups] = useState<any[]>([]);
44
+ const [isLoading, setIsLoading] = useState(false);
45
+
46
+ // Quick add
47
+ const [isQuickAddOpen, setIsQuickAddOpen] = useState(false);
48
+ const [quickAddData, setQuickAddData] = useState({
49
+ name: "",
50
+ abbreviation: "",
51
+ conversion_factor: "1",
52
+ uom_group_id: null as number | null,
53
+ uom_group_name: "",
54
+ base_uom_id: null as number | null,
55
+ base_uom_name: "",
56
+ status: "active"
57
+ });
58
+ const [isCreating, setIsCreating] = useState(false);
59
+
60
+ const fetchData = useCallback(async () => {
61
+ setIsLoading(true);
62
+ try {
63
+ const res = await getUomGroups({ include_uoms: true });
64
+ if (res.is_success) {
65
+ setGroups(res.result || []);
66
+ }
67
+ } catch {}
68
+ setIsLoading(false);
69
+ }, []);
70
+
71
+ const handleOpen = () => {
72
+ setSearch("");
73
+ setIsOpen(true);
74
+ fetchData();
75
+ };
76
+
77
+ const handleSelect = (uom: any) => {
78
+ onChange({ id: uom.id, name: uom.name, abbreviation: uom.abbreviation, uom_group_id: uom.uom_group_id });
79
+ setIsOpen(false);
80
+ };
81
+
82
+ const handleClear = () => {
83
+ onChange(null);
84
+ };
85
+
86
+ const handleQuickAdd = async () => {
87
+ if (!quickAddData.name.trim() || !quickAddData.abbreviation.trim() || !quickAddData.uom_group_id) {
88
+ toast.error("Please fill all required fields");
89
+ return;
90
+ }
91
+ setIsCreating(true);
92
+ try {
93
+ const res = await createUom({
94
+ name: quickAddData.name.trim(),
95
+ abbreviation: quickAddData.abbreviation.trim(),
96
+ conversion_factor: parseFloat(quickAddData.conversion_factor) || 1,
97
+ uom_group_id: quickAddData.uom_group_id,
98
+ base_uom_id: quickAddData.base_uom_id || null,
99
+ status: quickAddData.status,
100
+ });
101
+ if (res.is_success && res.result) {
102
+ toast.success("UOM created");
103
+ onChange({ id: res.result.id, name: res.result.name, abbreviation: res.result.abbreviation, uom_group_id: res.result.uom_group_id });
104
+ setIsQuickAddOpen(false);
105
+ setQuickAddData({ name: "", abbreviation: "", conversion_factor: "1", uom_group_id: null, uom_group_name: "", base_uom_id: null, base_uom_name: "", status: "active" });
106
+ setIsOpen(false);
107
+ } else {
108
+ toast.error(res.message || "Failed to create");
109
+ }
110
+ } catch (e: any) {
111
+ toast.error(e.message || "Error creating UOM");
112
+ }
113
+ setIsCreating(false);
114
+ };
115
+
116
+ const searchLower = search.toLowerCase();
117
+
118
+ const filteredGroups = groups
119
+ .filter(g => !uomGroupId || String(g.id) === String(uomGroupId))
120
+ .map(g => {
121
+ const matchedUoms = (g.uoms || [])
122
+ .filter((u: any) => !excludeId || String(u.id) !== String(excludeId))
123
+ .filter((u: any) => !baseUnitsOnly || !u.base_uom_id)
124
+ .filter((u: any) =>
125
+ u.name.toLowerCase().includes(searchLower) || u.abbreviation.toLowerCase().includes(searchLower)
126
+ );
127
+ return { ...g, uoms: matchedUoms };
128
+ })
129
+ .filter(g => g.uoms.length > 0 || g.name.toLowerCase().includes(searchLower));
130
+
131
+ return (
132
+ <>
133
+ <PickerTrigger
134
+ label={label}
135
+ value={displayValue || null}
136
+ placeholder={placeholder}
137
+ isRequired={isRequired}
138
+ onClick={handleOpen}
139
+ onClear={value ? handleClear : undefined}
140
+ />
141
+
142
+ <EntityPickerModal
143
+ isOpen={isOpen}
144
+ onClose={() => setIsOpen(false)}
145
+ onSelect={handleSelect}
146
+ title="Select Unit of Measure"
147
+ searchPlaceholder="Search UOMs…"
148
+ selectedId={value}
149
+ search={search}
150
+ onSearchChange={setSearch}
151
+ size="sm"
152
+ zIndex={pickerZIndex}
153
+ headerActions={
154
+ <button
155
+ type="button"
156
+ onClick={() => { setIsQuickAddOpen(true); setQuickAddData({ name: "", abbreviation: "", conversion_factor: "1", uom_group_id: null, uom_group_name: "", base_uom_id: null, base_uom_name: "", status: "active" }); }}
157
+ className="p-1.5 rounded-lg bg-primary-50 text-primary-600 hover:bg-primary-100 transition-colors flex-shrink-0"
158
+ title="Quick Add UOM"
159
+ >
160
+ <Plus size={16} strokeWidth={2.5} />
161
+ </button>
162
+ }
163
+ >
164
+ {isLoading ? (
165
+ <div className="py-12 text-center">
166
+ <div className="inline-block h-6 w-6 rounded-full border-2 border-gray-200 border-t-primary-500 animate-spin" />
167
+ <p className="mt-3 text-sm text-gray-400">Loading UOMs…</p>
168
+ </div>
169
+ ) : filteredGroups.length === 0 ? (
170
+ <div className="py-12 text-center">
171
+ <Ruler size={32} className="mx-auto text-gray-300 mb-2" />
172
+ <p className="text-sm text-gray-400">No units of measure found</p>
173
+ </div>
174
+ ) : (
175
+ <div className="space-y-4 py-1">
176
+ {filteredGroups.map(group => (
177
+ <div key={group.id} className="space-y-1">
178
+ <div className="px-3 py-1 text-xs font-semibold text-gray-500 uppercase tracking-wider bg-gray-50/50">
179
+ {group.name}
180
+ </div>
181
+ {group.uoms.map((uom: any) => (
182
+ <PickerItem
183
+ key={uom.id}
184
+ label={uom.name}
185
+ sublabel={uom.abbreviation}
186
+ isSelected={String(value) === String(uom.id)}
187
+ onClick={() => handleSelect(uom)}
188
+ />
189
+ ))}
190
+ </div>
191
+ ))}
192
+ </div>
193
+ )}
194
+ </EntityPickerModal>
195
+
196
+ {/* Quick Add Modal */}
197
+ <Modal
198
+ isOpen={isQuickAddOpen}
199
+ onClose={() => setIsQuickAddOpen(false)}
200
+ title="New UoM"
201
+ size="md"
202
+ zIndex={(pickerZIndex || 200) + 100}
203
+ >
204
+ <div className="space-y-4">
205
+ <UomGroupPicker
206
+ label="UOM Group"
207
+ isRequired
208
+ value={quickAddData.uom_group_id}
209
+ displayValue={quickAddData.uom_group_name || null}
210
+ pickerZIndex={400}
211
+ onChange={(g) => {
212
+ setQuickAddData({
213
+ ...quickAddData,
214
+ uom_group_id: g?.id ?? null,
215
+ uom_group_name: g?.name ?? "",
216
+ base_uom_id: null,
217
+ base_uom_name: "",
218
+ conversion_factor: "1"
219
+ });
220
+ }}
221
+ />
222
+ <div className="grid grid-cols-2 gap-4">
223
+ <Input
224
+ label="Code Name"
225
+ isRequired
226
+ placeholder="e.g. Kilogram"
227
+ value={quickAddData.name}
228
+ onChange={(e) => setQuickAddData({ ...quickAddData, name: e.target.value })}
229
+ />
230
+ <Input
231
+ label="Code"
232
+ isRequired
233
+ placeholder="e.g. KG"
234
+ value={quickAddData.abbreviation}
235
+ onChange={(e) => setQuickAddData({ ...quickAddData, abbreviation: e.target.value })}
236
+ />
237
+ </div>
238
+ {!baseUnitsOnly && (
239
+ <UomPicker
240
+ label="Base UOM"
241
+ placeholder="Select base unit…"
242
+ value={quickAddData.base_uom_id}
243
+ displayValue={quickAddData.base_uom_name || null}
244
+ pickerZIndex={400}
245
+ onChange={(uom) => {
246
+ let newGroupId = quickAddData.uom_group_id;
247
+ let newGroupName = quickAddData.uom_group_name;
248
+
249
+ if (uom?.uom_group_id && uom.uom_group_id !== quickAddData.uom_group_id) {
250
+ newGroupId = uom.uom_group_id;
251
+ const foundGroup = groups.find(g => g.id === uom.uom_group_id);
252
+ if (foundGroup) newGroupName = foundGroup.name;
253
+ }
254
+
255
+ setQuickAddData({
256
+ ...quickAddData,
257
+ base_uom_id: uom?.id ?? null,
258
+ base_uom_name: uom?.name ?? "",
259
+ uom_group_id: newGroupId,
260
+ uom_group_name: newGroupName,
261
+ });
262
+ }}
263
+ uomGroupId={quickAddData.uom_group_id}
264
+ />
265
+ )}
266
+ <div className="grid grid-cols-2 gap-4">
267
+ {!baseUnitsOnly && (
268
+ <Input
269
+ label="Conversion Factor"
270
+ isRequired
271
+ type="number"
272
+ placeholder="1"
273
+ value={quickAddData.conversion_factor}
274
+ onChange={(e) => setQuickAddData({ ...quickAddData, conversion_factor: e.target.value })}
275
+ />
276
+ )}
277
+ <Select
278
+ label="Status"
279
+ value={quickAddData.status}
280
+ onChange={(e) => setQuickAddData({ ...quickAddData, status: e.target.value })}
281
+ options={[
282
+ { value: "active", label: "Active" },
283
+ { value: "inactive", label: "Inactive" },
284
+ ]}
285
+ />
286
+ </div>
287
+ <ModalFooter>
288
+ <Button
289
+ variant="flat"
290
+ color="default"
291
+ onClick={() => setIsQuickAddOpen(false)}
292
+ >
293
+ Cancel
294
+ </Button>
295
+ <Button
296
+ onClick={handleQuickAdd}
297
+ isDisabled={isCreating || !quickAddData.name.trim() || !quickAddData.abbreviation.trim() || !quickAddData.uom_group_id}
298
+ >
299
+ {isCreating ? "Creating…" : "Create & Select"}
300
+ </Button>
301
+ </ModalFooter>
302
+ </div>
303
+ </Modal>
304
+ </>
305
+ );
306
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import React, { useState, useCallback } from "react";
4
+ import { Button, Input, Modal, ModalFooter } from "@apptimate/ui";
5
+ import { Plus, Warehouse } from "lucide-react";
6
+ import {
7
+ EntityPickerModal,
8
+ PickerItem,
9
+ PickerTrigger,
10
+ } from "./EntityPickerModal";
11
+ import { getWarehousesLookup } from "@apptimate/core-lib";
12
+ import toast from "react-hot-toast";
13
+
14
+ interface WarehousePickerProps {
15
+ value?: number | string | null;
16
+ displayValue?: string | null;
17
+ onChange: (warehouse: { id: number; name: string; code?: string; organization_id?: number } | null) => void;
18
+ label?: string;
19
+ placeholder?: string;
20
+ isRequired?: boolean;
21
+ fetchFromAllUserOrgs?: boolean;
22
+ }
23
+
24
+ export function WarehousePicker({
25
+ value,
26
+ displayValue,
27
+ onChange,
28
+ label = "Warehouse",
29
+ placeholder = "Select warehouse…",
30
+ isRequired = false,
31
+ fetchFromAllUserOrgs = false,
32
+ }: WarehousePickerProps) {
33
+ const [isOpen, setIsOpen] = useState(false);
34
+ const [search, setSearch] = useState("");
35
+ const [warehouses, setWarehouses] = useState<any[]>([]);
36
+ const [isLoading, setIsLoading] = useState(false);
37
+
38
+ const fetchData = useCallback(async () => {
39
+ setIsLoading(true);
40
+ try {
41
+ const res = await getWarehousesLookup(fetchFromAllUserOrgs);
42
+ if (res.is_success) {
43
+ setWarehouses(res.result || []);
44
+ // Auto-select if there is exactly 1 warehouse and no value is currently selected
45
+ if (res.result?.length === 1 && !value) {
46
+ onChange({ id: res.result[0].id, name: res.result[0].name, code: res.result[0].code, organization_id: res.result[0].organization_id });
47
+ }
48
+ }
49
+ } catch {}
50
+ setIsLoading(false);
51
+ }, [value, onChange, fetchFromAllUserOrgs]);
52
+
53
+ // Fetch on mount to pre-populate and auto-select if applicable
54
+ React.useEffect(() => {
55
+ fetchData();
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, []);
58
+
59
+ const handleOpen = () => {
60
+ setSearch("");
61
+ setIsOpen(true);
62
+ fetchData();
63
+ };
64
+
65
+ const handleSelect = (wh: any) => {
66
+ onChange({ id: wh.id, name: wh.name, code: wh.code, organization_id: wh.organization_id });
67
+ setIsOpen(false);
68
+ };
69
+
70
+ const handleClear = () => {
71
+ onChange(null);
72
+ };
73
+
74
+ const searchLower = (search || "").toLowerCase();
75
+ const filtered = warehouses.filter(
76
+ (w) =>
77
+ (w.name || "").toLowerCase().includes(searchLower) ||
78
+ (w.code || "").toLowerCase().includes(searchLower) ||
79
+ (w.organization?.name || "").toLowerCase().includes(searchLower)
80
+ );
81
+
82
+ return (
83
+ <>
84
+ <PickerTrigger
85
+ label={label}
86
+ value={displayValue || null}
87
+ placeholder={placeholder}
88
+ isRequired={isRequired}
89
+ onClick={handleOpen}
90
+ onClear={value ? handleClear : undefined}
91
+ />
92
+
93
+ <EntityPickerModal
94
+ isOpen={isOpen}
95
+ onClose={() => setIsOpen(false)}
96
+ onSelect={handleSelect}
97
+ title={`Select ${label}`}
98
+ searchPlaceholder={`Search warehouses…`}
99
+ selectedId={value}
100
+ search={search}
101
+ onSearchChange={setSearch}
102
+ size="sm"
103
+ >
104
+ {isLoading ? (
105
+ <div className="py-12 text-center">
106
+ <div className="inline-block h-6 w-6 rounded-full border-2 border-gray-200 border-t-primary-500 animate-spin" />
107
+ <p className="mt-3 text-sm text-gray-400">Loading warehouses…</p>
108
+ </div>
109
+ ) : filtered.length === 0 ? (
110
+ <div className="py-12 text-center">
111
+ <Warehouse size={32} className="mx-auto text-gray-300 mb-2" />
112
+ <p className="text-sm text-gray-400">No warehouses found</p>
113
+ </div>
114
+ ) : (
115
+ <div className="space-y-0.5 py-1">
116
+ {filtered.map((wh) => (
117
+ <PickerItem
118
+ key={wh.id}
119
+ label={wh.name}
120
+ sublabel={
121
+ [
122
+ wh.code ? `Code: ${wh.code}` : null,
123
+ wh.organization?.name ? `Org: ${wh.organization.name}` : null
124
+ ].filter(Boolean).join(" | ") || undefined
125
+ }
126
+ isSelected={String(value) === String(wh.id)}
127
+ onClick={() => handleSelect(wh)}
128
+ />
129
+ ))}
130
+ </div>
131
+ )}
132
+ </EntityPickerModal>
133
+ </>
134
+ );
135
+ }
@@ -0,0 +1,11 @@
1
+ export * from './BrandPicker';
2
+ export * from './CategoryPicker';
3
+ export * from './EntityPickerModal';
4
+ export * from './UomGroupPicker';
5
+ export * from './UomPicker';
6
+ export * from './PartyPicker';
7
+ export * from './WarehousePicker';
8
+ export * from './TransactionItemPicker';
9
+ export * from './modals/BatchSelectionModal';
10
+ export * from './modals/SerialSelectionModal';
11
+ export * from './modals/types';