@apptimate/ui 5.1.0 → 5.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 +3 -2
- package/src/base-components/ChartOfAccountPicker.tsx +203 -0
- package/src/base-components/ColorPicker.tsx +204 -0
- package/src/base-components/Modal.tsx +17 -7
- package/src/base-components/RichEmailEditor.tsx +137 -0
- package/src/common-components/DashboardLayout.tsx +17 -1
- package/src/common-components/attendance-shifts/AttendanceTimesheets.tsx +43 -0
- package/src/common-components/attendance-shifts/CalendarLegend.tsx +27 -0
- package/src/common-components/attendance-shifts/CancelShiftModal.tsx +148 -0
- package/src/common-components/attendance-shifts/DailyOverview.tsx +198 -0
- package/src/common-components/attendance-shifts/DailySchedulePanel.tsx +139 -0
- package/src/common-components/attendance-shifts/ExchangeShiftModal.tsx +132 -0
- package/src/common-components/attendance-shifts/ImportExcelModal.tsx +183 -0
- package/src/common-components/attendance-shifts/ManualPunchModal.tsx +139 -0
- package/src/common-components/attendance-shifts/PendingActionsPanel.tsx +321 -0
- package/src/common-components/attendance-shifts/ShiftCalendar.tsx +451 -0
- package/src/common-components/attendance-shifts/ShiftTemplateForm.tsx +249 -0
- package/src/common-components/attendance-shifts/SummaryStrip.tsx +81 -0
- package/src/common-components/item-wizard/ItemFormWizard.tsx +8 -1
- package/src/common-components/item-wizard/SkuConfigModal.tsx +6 -1
- package/src/common-components/pickers/BrandPicker.tsx +5 -1
- package/src/common-components/pickers/CategoryPicker.tsx +5 -1
- package/src/common-components/pickers/EmployeePicker.tsx +120 -0
- package/src/common-components/pickers/EntityPickerModal.tsx +10 -5
- package/src/common-components/pickers/PartyPicker.tsx +5 -1
- package/src/common-components/pickers/UomGroupPicker.tsx +5 -1
- package/src/common-components/pickers/UomPicker.tsx +6 -1
- package/src/common-components/pickers/WarehousePicker.tsx +5 -1
- package/src/components/shared/CustomerSelectorComponent.tsx +7 -1
- package/src/components/shared/ImageUploadComponent.tsx +4 -1
- package/src/components/shared/PaymentModeComponent.tsx +6 -1
- package/src/components/shared/ProductSelectorComponent.tsx +7 -1
- package/src/index.tsx +19 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react";
|
|
4
|
+
import { Modal, Input, Button } from "../../index";
|
|
5
|
+
import toast from "react-hot-toast";
|
|
6
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
7
|
+
import { getShiftTemplate, createShiftTemplate, updateShiftTemplate } from "@apptimate/core-lib";
|
|
8
|
+
|
|
9
|
+
interface ShiftTemplateFormProps {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onSuccess: () => void;
|
|
13
|
+
templateId: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function ShiftTemplateForm({ isOpen, onClose, onSuccess, templateId }: ShiftTemplateFormProps) {
|
|
17
|
+
const isNew = !templateId;
|
|
18
|
+
|
|
19
|
+
const [loading, setLoading] = useState(!isNew);
|
|
20
|
+
const [saving, setSaving] = useState(false);
|
|
21
|
+
|
|
22
|
+
const [formData, setFormData] = useState<any>({
|
|
23
|
+
name: "",
|
|
24
|
+
code: "",
|
|
25
|
+
break_minutes: 0,
|
|
26
|
+
grace_late_min: 10,
|
|
27
|
+
grace_early_min: 10,
|
|
28
|
+
is_active: true,
|
|
29
|
+
segments: [{ start_time: "", end_time: "" }],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isOpen) {
|
|
34
|
+
if (!isNew && templateId) {
|
|
35
|
+
loadTemplate(templateId);
|
|
36
|
+
} else {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
setFormData({
|
|
41
|
+
name: "",
|
|
42
|
+
code: "",
|
|
43
|
+
break_minutes: 0,
|
|
44
|
+
grace_late_min: 10,
|
|
45
|
+
grace_early_min: 10,
|
|
46
|
+
is_active: true,
|
|
47
|
+
segments: [{ start_time: "", end_time: "" }],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [isOpen, templateId]);
|
|
51
|
+
|
|
52
|
+
const loadTemplate = async (id: number) => {
|
|
53
|
+
try {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
const res = await getShiftTemplate(id);
|
|
56
|
+
if (res.is_success && res.result) {
|
|
57
|
+
// Time format might be HH:mm:ss, truncate to HH:mm for input type="time"
|
|
58
|
+
const formattedSegments = (res.result.segments || []).map((seg: any) => ({
|
|
59
|
+
start_time: seg.start_time ? seg.start_time.substring(0, 5) : "",
|
|
60
|
+
end_time: seg.end_time ? seg.end_time.substring(0, 5) : "",
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
setFormData({
|
|
64
|
+
name: res.result.name || "",
|
|
65
|
+
code: res.result.code || "",
|
|
66
|
+
break_minutes: res.result.break_minutes || 0,
|
|
67
|
+
grace_late_min: res.result.grace_late_min || 10,
|
|
68
|
+
grace_early_min: res.result.grace_early_min || 10,
|
|
69
|
+
is_active: res.result.is_active ?? true,
|
|
70
|
+
segments: formattedSegments.length > 0 ? formattedSegments : [{ start_time: "", end_time: "" }],
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
toast.error("Failed to load template");
|
|
74
|
+
onClose();
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
toast.error("Error loading template");
|
|
78
|
+
onClose();
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleSegmentChange = (index: number, field: string, value: string) => {
|
|
85
|
+
const updated = [...formData.segments];
|
|
86
|
+
updated[index][field] = value;
|
|
87
|
+
setFormData({ ...formData, segments: updated });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const addSegment = () => {
|
|
91
|
+
setFormData({ ...formData, segments: [...formData.segments, { start_time: "", end_time: "" }] });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const removeSegment = (index: number) => {
|
|
95
|
+
if (formData.segments.length === 1) return;
|
|
96
|
+
const updated = formData.segments.filter((_: any, i: number) => i !== index);
|
|
97
|
+
setFormData({ ...formData, segments: updated });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const saveTemplate = async () => {
|
|
101
|
+
if (!formData.name) {
|
|
102
|
+
toast.error("Template name is required");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!formData.code) {
|
|
106
|
+
toast.error("Template code is required");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// append :00 for seconds if missing
|
|
111
|
+
const payload = {
|
|
112
|
+
...formData,
|
|
113
|
+
segments: formData.segments.map((seg: any) => ({
|
|
114
|
+
start_time: seg.start_time.length === 5 ? `${seg.start_time}:00` : seg.start_time,
|
|
115
|
+
end_time: seg.end_time.length === 5 ? `${seg.end_time}:00` : seg.end_time,
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
setSaving(true);
|
|
121
|
+
const res = isNew
|
|
122
|
+
? await createShiftTemplate(payload)
|
|
123
|
+
: await updateShiftTemplate(templateId as number, payload);
|
|
124
|
+
|
|
125
|
+
if (res.is_success) {
|
|
126
|
+
toast.success(`Template ${isNew ? "created" : "updated"}`);
|
|
127
|
+
onSuccess();
|
|
128
|
+
} else {
|
|
129
|
+
toast.error(res.message || "Failed to save template");
|
|
130
|
+
}
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
toast.error(e.message || "Error saving template");
|
|
133
|
+
} finally {
|
|
134
|
+
setSaving(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Modal
|
|
140
|
+
isOpen={isOpen}
|
|
141
|
+
onClose={onClose}
|
|
142
|
+
title={isNew ? "Add Shift Template" : "Edit Shift Template"}
|
|
143
|
+
size="2xl"
|
|
144
|
+
footer={
|
|
145
|
+
<div className="flex justify-end gap-2">
|
|
146
|
+
<Button variant="flat" color="secondary" onClick={onClose}>Cancel</Button>
|
|
147
|
+
<Button color="primary" onClick={saveTemplate} isLoading={saving}>Save Template</Button>
|
|
148
|
+
</div>
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
{loading ? (
|
|
152
|
+
<div className="py-20 flex justify-center">
|
|
153
|
+
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="space-y-6">
|
|
157
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
158
|
+
<Input
|
|
159
|
+
label="Template Name *"
|
|
160
|
+
value={formData.name}
|
|
161
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
162
|
+
placeholder="e.g. Day Shift"
|
|
163
|
+
/>
|
|
164
|
+
<Input
|
|
165
|
+
label="Code *"
|
|
166
|
+
value={formData.code}
|
|
167
|
+
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
|
|
168
|
+
placeholder="e.g. DAY"
|
|
169
|
+
/>
|
|
170
|
+
<Input
|
|
171
|
+
label="Break Minutes"
|
|
172
|
+
type="number"
|
|
173
|
+
min="0"
|
|
174
|
+
value={formData.break_minutes}
|
|
175
|
+
onChange={(e) => setFormData({ ...formData, break_minutes: parseInt(e.target.value) || 0 })}
|
|
176
|
+
/>
|
|
177
|
+
<div className="grid grid-cols-2 gap-2">
|
|
178
|
+
<Input
|
|
179
|
+
label="Grace Late (Min)"
|
|
180
|
+
type="number"
|
|
181
|
+
min="0"
|
|
182
|
+
value={formData.grace_late_min}
|
|
183
|
+
onChange={(e) => setFormData({ ...formData, grace_late_min: parseInt(e.target.value) || 0 })}
|
|
184
|
+
/>
|
|
185
|
+
<Input
|
|
186
|
+
label="Grace Early (Min)"
|
|
187
|
+
type="number"
|
|
188
|
+
min="0"
|
|
189
|
+
value={formData.grace_early_min}
|
|
190
|
+
onChange={(e) => setFormData({ ...formData, grace_early_min: parseInt(e.target.value) || 0 })}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="border-t border-gray-100 pt-6">
|
|
196
|
+
<div className="flex items-center justify-between mb-4">
|
|
197
|
+
<h3 className="font-bold text-gray-900">Time Segments</h3>
|
|
198
|
+
<Button
|
|
199
|
+
size="small"
|
|
200
|
+
variant="solid"
|
|
201
|
+
className="!bg-black !text-white hover:!bg-gray-800 !border-none"
|
|
202
|
+
onClick={addSegment}
|
|
203
|
+
icon={<Plus size={14} strokeWidth={2.5} />}
|
|
204
|
+
iconPosition="left"
|
|
205
|
+
>
|
|
206
|
+
Add Segment
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="space-y-3">
|
|
211
|
+
{formData.segments.map((segment: any, index: number) => (
|
|
212
|
+
<div key={index} className="flex items-end gap-3 p-3 bg-gray-50 border border-gray-200 rounded-lg">
|
|
213
|
+
<div className="flex-1">
|
|
214
|
+
<Input
|
|
215
|
+
label="Start Time *"
|
|
216
|
+
type="time"
|
|
217
|
+
value={segment.start_time}
|
|
218
|
+
onChange={(e) => handleSegmentChange(index, "start_time", e.target.value)}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex-1">
|
|
222
|
+
<Input
|
|
223
|
+
label="End Time *"
|
|
224
|
+
type="time"
|
|
225
|
+
value={segment.end_time}
|
|
226
|
+
onChange={(e) => handleSegmentChange(index, "end_time", e.target.value)}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
{formData.segments.length > 1 && (
|
|
230
|
+
<button
|
|
231
|
+
onClick={() => removeSegment(index)}
|
|
232
|
+
className="flex items-center justify-center h-[38px] w-[38px] text-gray-400 hover:text-danger-500 transition-colors bg-white border border-gray-200 rounded-[10px] hover:border-danger-200 hover:bg-danger-50"
|
|
233
|
+
title="Remove Segment"
|
|
234
|
+
>
|
|
235
|
+
<Trash2 size={16} />
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
<p className="text-xs text-gray-500 mt-3">
|
|
242
|
+
Add multiple segments to create a split shift (e.g., 08:00 - 12:00 and 13:00 - 18:00).
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</Modal>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Briefcase,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
XCircle,
|
|
8
|
+
CalendarOff,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
Palmtree,
|
|
11
|
+
CalendarDays,
|
|
12
|
+
Clock,
|
|
13
|
+
Hourglass,
|
|
14
|
+
Timer,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
|
|
17
|
+
export interface SummaryData {
|
|
18
|
+
workingDays: number;
|
|
19
|
+
present: number;
|
|
20
|
+
absent: number;
|
|
21
|
+
leave: number;
|
|
22
|
+
lop: number;
|
|
23
|
+
holidays: number;
|
|
24
|
+
weeklyOffs: number;
|
|
25
|
+
otDays: number;
|
|
26
|
+
otHours: number;
|
|
27
|
+
lates: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SummaryStripProps {
|
|
31
|
+
data: SummaryData;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const statConfig = [
|
|
35
|
+
{ key: "workingDays", label: "Working Days", icon: Briefcase, color: "text-blue-600", bg: "bg-blue-50", ring: "ring-blue-100" },
|
|
36
|
+
{ key: "present", label: "Present", icon: CheckCircle2, color: "text-emerald-600", bg: "bg-emerald-50", ring: "ring-emerald-100" },
|
|
37
|
+
{ key: "absent", label: "Absent", icon: XCircle, color: "text-red-500", bg: "bg-red-50", ring: "ring-red-100" },
|
|
38
|
+
{ key: "leave", label: "Leave", icon: CalendarOff, color: "text-amber-600", bg: "bg-amber-50", ring: "ring-amber-100" },
|
|
39
|
+
{ key: "lop", label: "LOP", icon: AlertTriangle, color: "text-orange-600", bg: "bg-orange-50", ring: "ring-orange-100" },
|
|
40
|
+
{ key: "holidays", label: "Holidays", icon: Palmtree, color: "text-indigo-600", bg: "bg-indigo-50", ring: "ring-indigo-100" },
|
|
41
|
+
{ key: "weeklyOffs", label: "Weekly Offs", icon: CalendarDays, color: "text-gray-500", bg: "bg-gray-50", ring: "ring-gray-200" },
|
|
42
|
+
{ key: "otDays", label: "OT Days", icon: Clock, color: "text-purple-600", bg: "bg-purple-50", ring: "ring-purple-100" },
|
|
43
|
+
{ key: "otHours", label: "OT Hours", icon: Hourglass, color: "text-violet-600", bg: "bg-violet-50", ring: "ring-violet-100" },
|
|
44
|
+
{ key: "lates", label: "Lates", icon: Timer, color: "text-rose-500", bg: "bg-rose-50", ring: "ring-rose-100" },
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export default function SummaryStrip({ data }: SummaryStripProps) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="relative overflow-hidden rounded-[16px] border border-gray-200/60 shadow-sm bg-white/80 backdrop-blur-sm">
|
|
50
|
+
{/* Subtle gradient background */}
|
|
51
|
+
<div className="absolute inset-0 bg-gradient-to-r from-slate-50/80 via-white/60 to-blue-50/40 pointer-events-none" />
|
|
52
|
+
|
|
53
|
+
<div className="relative flex items-stretch gap-0 overflow-x-auto scrollbar-hide">
|
|
54
|
+
{statConfig.map((stat, idx) => {
|
|
55
|
+
const Icon = stat.icon;
|
|
56
|
+
const value = data[stat.key as keyof SummaryData];
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
key={stat.key}
|
|
60
|
+
className={`flex items-center gap-2.5 px-4 py-3.5 min-w-[130px] flex-1 transition-all duration-200 hover:bg-gray-50/80 group cursor-default ${
|
|
61
|
+
idx < statConfig.length - 1 ? "border-r border-gray-100" : ""
|
|
62
|
+
}`}
|
|
63
|
+
>
|
|
64
|
+
<div className={`w-8 h-8 rounded-lg ${stat.bg} ring-1 ${stat.ring} flex items-center justify-center flex-shrink-0 transition-transform duration-200 group-hover:scale-110`}>
|
|
65
|
+
<Icon size={15} className={stat.color} />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex flex-col min-w-0">
|
|
68
|
+
<span className="text-[18px] font-bold text-gray-900 leading-tight tracking-tight">
|
|
69
|
+
{value ?? 0}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="text-[10.5px] font-medium text-gray-400 uppercase tracking-wider truncate leading-tight">
|
|
72
|
+
{stat.label}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
|
-
import { Input
|
|
4
|
+
import { Input } from '../../base-components/Input';
|
|
5
|
+
import { Select } from '../../base-components/Select';
|
|
6
|
+
import { Button } from '../../base-components/Button';
|
|
7
|
+
import { WizardModal } from '../../base-components/WizardModal';
|
|
8
|
+
import { Checkbox } from '../../base-components/Checkbox';
|
|
9
|
+
import { HintIcon } from '../../base-components/HintIcon';
|
|
10
|
+
import { TagsInput } from '../../base-components/TagsInput';
|
|
11
|
+
|
|
5
12
|
import { CategoryPicker, BrandPicker } from "../pickers";
|
|
6
13
|
import { UomPicker } from "../pickers/UomPicker";
|
|
7
14
|
import { ChevronLeft, ChevronRight, Settings, Trash, Trash2, Plus } from "lucide-react";
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from "react";
|
|
4
|
-
import { Modal
|
|
4
|
+
import { Modal } from '../../base-components/Modal';
|
|
5
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
6
|
+
import { Button } from '../../base-components/Button';
|
|
7
|
+
import { Select } from '../../base-components/Select';
|
|
8
|
+
import { Input } from '../../base-components/Input';
|
|
9
|
+
|
|
5
10
|
import { saveInventorySetting, getInventorySetting, getItemCategoryCount } from "@apptimate/core-lib";
|
|
6
11
|
import { Trash2, Plus } from "lucide-react";
|
|
7
12
|
import toast from "react-hot-toast";
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
|
|
5
9
|
import { Plus, Package } from "lucide-react";
|
|
6
10
|
import {
|
|
7
11
|
EntityPickerModal,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback, useEffect } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
|
|
5
9
|
import { Plus, List, GitBranch, ChevronRight, ChevronDown, FolderOpen } from "lucide-react";
|
|
6
10
|
import {
|
|
7
11
|
EntityPickerModal,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback } from "react";
|
|
4
|
+
import { Users } from "lucide-react";
|
|
5
|
+
import { EntityPickerModal, PickerItem, PickerTrigger } from "./EntityPickerModal";
|
|
6
|
+
import { getEmployeeLookup } from "@apptimate/core-lib";
|
|
7
|
+
|
|
8
|
+
interface EmployeePickerProps {
|
|
9
|
+
value?: number | string | null;
|
|
10
|
+
displayValue?: string | null;
|
|
11
|
+
onChange: (emp: { id: number; name: string; code?: string } | null) => void;
|
|
12
|
+
label?: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
isRequired?: boolean;
|
|
15
|
+
excludeId?: number | string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function EmployeePicker({
|
|
19
|
+
value,
|
|
20
|
+
displayValue,
|
|
21
|
+
onChange,
|
|
22
|
+
label = "Employee",
|
|
23
|
+
placeholder = "Select employee...",
|
|
24
|
+
isRequired = false,
|
|
25
|
+
excludeId,
|
|
26
|
+
}: EmployeePickerProps) {
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
const [search, setSearch] = useState("");
|
|
29
|
+
const [employees, setEmployees] = useState<any[]>([]);
|
|
30
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
31
|
+
|
|
32
|
+
const fetchData = useCallback(async () => {
|
|
33
|
+
setIsLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
const res = await getEmployeeLookup();
|
|
36
|
+
if (res.is_success && res.result) {
|
|
37
|
+
setEmployees(res.result);
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
setIsLoading(false);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const handleOpen = () => {
|
|
44
|
+
setSearch("");
|
|
45
|
+
setIsOpen(true);
|
|
46
|
+
fetchData();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleSelect = (emp: any) => {
|
|
50
|
+
const fullName = `${emp.first_name} ${emp.last_name || ""}`.trim();
|
|
51
|
+
onChange({ id: emp.id, name: fullName, code: emp.employee_code });
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleClear = () => {
|
|
56
|
+
onChange(null);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const searchLower = (search || "").toLowerCase();
|
|
60
|
+
|
|
61
|
+
const filtered = employees.filter((e) => {
|
|
62
|
+
if (excludeId && String(e.id) === String(excludeId)) return false;
|
|
63
|
+
|
|
64
|
+
const fullName = `${e.first_name} ${e.last_name || ""}`.trim().toLowerCase();
|
|
65
|
+
const code = (e.employee_code || "").toLowerCase();
|
|
66
|
+
return fullName.includes(searchLower) || code.includes(searchLower);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<PickerTrigger
|
|
72
|
+
label={label}
|
|
73
|
+
value={displayValue || null}
|
|
74
|
+
placeholder={placeholder}
|
|
75
|
+
isRequired={isRequired}
|
|
76
|
+
onClick={handleOpen}
|
|
77
|
+
onClear={value ? handleClear : undefined}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<EntityPickerModal
|
|
81
|
+
isOpen={isOpen}
|
|
82
|
+
onClose={() => setIsOpen(false)}
|
|
83
|
+
onSelect={handleSelect}
|
|
84
|
+
title={`Select ${label}`}
|
|
85
|
+
searchPlaceholder="Search employees..."
|
|
86
|
+
selectedId={value}
|
|
87
|
+
search={search}
|
|
88
|
+
onSearchChange={setSearch}
|
|
89
|
+
size="sm"
|
|
90
|
+
>
|
|
91
|
+
{isLoading ? (
|
|
92
|
+
<div className="py-12 text-center">
|
|
93
|
+
<div className="inline-block h-6 w-6 rounded-full border-2 border-gray-200 border-t-primary-500 animate-spin" />
|
|
94
|
+
<p className="mt-3 text-sm text-gray-400">Loading employees...</p>
|
|
95
|
+
</div>
|
|
96
|
+
) : filtered.length === 0 ? (
|
|
97
|
+
<div className="py-12 text-center">
|
|
98
|
+
<Users size={32} className="mx-auto text-gray-300 mb-2" />
|
|
99
|
+
<p className="text-sm text-gray-400">No employees found</p>
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="space-y-0.5 py-1">
|
|
103
|
+
{filtered.map((emp) => {
|
|
104
|
+
const fullName = `${emp.first_name} ${emp.last_name || ""}`.trim();
|
|
105
|
+
return (
|
|
106
|
+
<PickerItem
|
|
107
|
+
key={emp.id}
|
|
108
|
+
label={fullName}
|
|
109
|
+
sublabel={emp.employee_code ? `ID: ${emp.employee_code}` : undefined}
|
|
110
|
+
isSelected={String(value) === String(emp.id)}
|
|
111
|
+
onClick={() => handleSelect(emp)}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</EntityPickerModal>
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, ReactNode } from "react";
|
|
4
|
-
import { Modal
|
|
4
|
+
import { Modal } from '../../base-components/Modal';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Button } from '../../base-components/Button';
|
|
7
|
+
|
|
5
8
|
import { Search, Plus, X, Check } from "lucide-react";
|
|
6
9
|
|
|
7
10
|
/* ──────────────────────────────────────────────────────────────────────────
|
|
@@ -189,10 +192,12 @@ export function PickerTrigger({
|
|
|
189
192
|
}: PickerTriggerProps) {
|
|
190
193
|
return (
|
|
191
194
|
<div className="flex flex-col gap-1.5 w-full">
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
{label && (
|
|
196
|
+
<label className="text-[11px] font-bold text-foreground-subtle uppercase tracking-wider">
|
|
197
|
+
{label}
|
|
198
|
+
{isRequired && <span className="text-danger-alt ml-0.5">*</span>}
|
|
199
|
+
</label>
|
|
200
|
+
)}
|
|
196
201
|
<button
|
|
197
202
|
type="button"
|
|
198
203
|
disabled={disabled}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback, useEffect } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
|
|
5
9
|
import { Plus, User } from "lucide-react";
|
|
6
10
|
import {
|
|
7
11
|
EntityPickerModal,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback, useEffect } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
|
|
5
9
|
import { Plus, Folder } from "lucide-react";
|
|
6
10
|
import {
|
|
7
11
|
EntityPickerModal,
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
import { Select } from '../../base-components/Select';
|
|
9
|
+
|
|
5
10
|
import { Plus, Ruler } from "lucide-react";
|
|
6
11
|
import {
|
|
7
12
|
EntityPickerModal,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
|
-
import { Button
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Modal } from '../../base-components/Modal';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
|
|
5
9
|
import { Plus, Warehouse } from "lucide-react";
|
|
6
10
|
import {
|
|
7
11
|
EntityPickerModal,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
-
import { Modal
|
|
3
|
+
import { Modal } from '../../base-components/Modal';
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Input } from '../../base-components/Input';
|
|
6
|
+
import { Pagination } from '../../base-components/Pagination';
|
|
7
|
+
import { EmptyState } from '../../base-components/EmptyState';
|
|
8
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
9
|
+
|
|
4
10
|
import { Search, User, Phone, Mail, MapPin, X } from "lucide-react";
|
|
5
11
|
import toast from "react-hot-toast";
|
|
6
12
|
import { sendRequest, IApiResponse } from "@apptimate/core-lib";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React, { useState, useRef, useCallback } from "react";
|
|
3
|
-
import { Button
|
|
3
|
+
import { Button } from '../../base-components/Button';
|
|
4
|
+
import { Modal } from '../../base-components/Modal';
|
|
5
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
6
|
+
|
|
4
7
|
import { Upload, X, Star, GripVertical, ImagePlus, Trash2 } from "lucide-react";
|
|
5
8
|
import toast from "react-hot-toast";
|
|
6
9
|
import { sendRequest } from "@apptimate/core-lib";
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { Input
|
|
4
|
+
import { Input } from '../../base-components/Input';
|
|
5
|
+
import { Modal } from '../../base-components/Modal';
|
|
6
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
7
|
+
import { Button } from '../../base-components/Button';
|
|
8
|
+
import { Badge } from '../../base-components/Badge';
|
|
9
|
+
|
|
5
10
|
import { Banknote, CreditCard, Building2, Smartphone, FileText, Plus, X, Search, ArrowRight, AlertTriangle, Clock } from "lucide-react";
|
|
6
11
|
|
|
7
12
|
// ── Types ────────────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
-
import { Modal
|
|
3
|
+
import { Modal } from '../../base-components/Modal';
|
|
4
|
+
import { Button } from '../../base-components/Button';
|
|
5
|
+
import { Pagination } from '../../base-components/Pagination';
|
|
6
|
+
import { EmptyState } from '../../base-components/EmptyState';
|
|
7
|
+
import { ModalFooter } from '../../base-components/Modal';
|
|
8
|
+
import { Badge } from '../../base-components/Badge';
|
|
9
|
+
|
|
4
10
|
import { Search, Package, X, Tag, Barcode } from "lucide-react";
|
|
5
11
|
import toast from "react-hot-toast";
|
|
6
12
|
import { sendRequest, IApiResponse } from "@apptimate/core-lib";
|