@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.
Files changed (33) hide show
  1. package/package.json +3 -2
  2. package/src/base-components/ChartOfAccountPicker.tsx +203 -0
  3. package/src/base-components/ColorPicker.tsx +204 -0
  4. package/src/base-components/Modal.tsx +17 -7
  5. package/src/base-components/RichEmailEditor.tsx +137 -0
  6. package/src/common-components/DashboardLayout.tsx +17 -1
  7. package/src/common-components/attendance-shifts/AttendanceTimesheets.tsx +43 -0
  8. package/src/common-components/attendance-shifts/CalendarLegend.tsx +27 -0
  9. package/src/common-components/attendance-shifts/CancelShiftModal.tsx +148 -0
  10. package/src/common-components/attendance-shifts/DailyOverview.tsx +198 -0
  11. package/src/common-components/attendance-shifts/DailySchedulePanel.tsx +139 -0
  12. package/src/common-components/attendance-shifts/ExchangeShiftModal.tsx +132 -0
  13. package/src/common-components/attendance-shifts/ImportExcelModal.tsx +183 -0
  14. package/src/common-components/attendance-shifts/ManualPunchModal.tsx +139 -0
  15. package/src/common-components/attendance-shifts/PendingActionsPanel.tsx +321 -0
  16. package/src/common-components/attendance-shifts/ShiftCalendar.tsx +451 -0
  17. package/src/common-components/attendance-shifts/ShiftTemplateForm.tsx +249 -0
  18. package/src/common-components/attendance-shifts/SummaryStrip.tsx +81 -0
  19. package/src/common-components/item-wizard/ItemFormWizard.tsx +8 -1
  20. package/src/common-components/item-wizard/SkuConfigModal.tsx +6 -1
  21. package/src/common-components/pickers/BrandPicker.tsx +5 -1
  22. package/src/common-components/pickers/CategoryPicker.tsx +5 -1
  23. package/src/common-components/pickers/EmployeePicker.tsx +120 -0
  24. package/src/common-components/pickers/EntityPickerModal.tsx +10 -5
  25. package/src/common-components/pickers/PartyPicker.tsx +5 -1
  26. package/src/common-components/pickers/UomGroupPicker.tsx +5 -1
  27. package/src/common-components/pickers/UomPicker.tsx +6 -1
  28. package/src/common-components/pickers/WarehousePicker.tsx +5 -1
  29. package/src/components/shared/CustomerSelectorComponent.tsx +7 -1
  30. package/src/components/shared/ImageUploadComponent.tsx +4 -1
  31. package/src/components/shared/PaymentModeComponent.tsx +6 -1
  32. package/src/components/shared/ProductSelectorComponent.tsx +7 -1
  33. package/src/index.tsx +19 -0
@@ -0,0 +1,132 @@
1
+ import React, { useState } from 'react';
2
+ import { Modal, Button } from '../../index';
3
+ import { Clock, MapPin, Send } from 'lucide-react';
4
+ import { format } from 'date-fns';
5
+ import { EmployeePicker } from '../pickers/EmployeePicker';
6
+
7
+ interface ExchangeShiftModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ day: Date | null;
11
+ shift: any;
12
+ segmentNo: number | null;
13
+ onConfirm: (responderId: number, note: string) => void;
14
+ }
15
+
16
+ export default function ExchangeShiftModal({ isOpen, onClose, day, shift, segmentNo, onConfirm }: ExchangeShiftModalProps) {
17
+ const [selectedEmployee, setSelectedEmployee] = useState<{ id: number; name: string } | null>(null);
18
+ const [note, setNote] = useState("");
19
+
20
+ if (!isOpen || !day || !shift || segmentNo === null) return null;
21
+
22
+ const segment = shift.segmentsInfo.find((s: any) => s.segment_no === segmentNo);
23
+
24
+ const handleConfirm = () => {
25
+ if (selectedEmployee) {
26
+ onConfirm(selectedEmployee.id, note);
27
+ setSelectedEmployee(null);
28
+ setNote("");
29
+ }
30
+ };
31
+
32
+ return (
33
+ <Modal isOpen={isOpen} onClose={onClose} size="xl">
34
+ <div className="flex bg-gray-50 h-[500px] overflow-hidden rounded-[16px]">
35
+ {/* Left Panel - Shift Details */}
36
+ <div className="w-[350px] bg-white border-r border-gray-200 p-6 flex flex-col h-full relative">
37
+ <div className="absolute left-0 top-0 bottom-0 w-1.5 bg-[#0D6EFD]"></div>
38
+
39
+ <div className="bg-[#0D6EFD] text-white text-[10px] font-bold uppercase tracking-wider mb-4 py-1 px-3 rounded-full inline-flex self-start">
40
+ Your Shift
41
+ </div>
42
+ <h2 className="text-3xl font-bold text-gray-900 mb-8">{shift.template?.name || 'Shift'}</h2>
43
+
44
+ <div className="space-y-6 flex-1">
45
+ <div className="flex gap-4">
46
+ <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0 text-[#0D6EFD]">
47
+ <Clock className="w-5 h-5" />
48
+ </div>
49
+ <div>
50
+ <div className="text-lg font-bold text-gray-900">{segment?.str}</div>
51
+ <div className="text-sm text-gray-500">{format(day, 'EEEE, MMM d, yyyy')}</div>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="flex gap-4">
56
+ <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0 text-[#0D6EFD]">
57
+ <MapPin className="w-5 h-5" />
58
+ </div>
59
+ <div>
60
+ <div className="text-lg font-bold text-gray-900">Main Wing, Level 2</div>
61
+ <div className="text-sm text-gray-500">Senior Registered Nurse</div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Right Panel - Employee Selection */}
68
+ <div className="flex-1 p-8 flex flex-col bg-white">
69
+ <div className="flex justify-between items-center mb-6">
70
+ <h2 className="text-2xl font-bold text-gray-900">Exchange Shift</h2>
71
+ <button onClick={onClose} className="text-sm font-semibold text-[#0D6EFD] hover:underline">
72
+ ← Back to Schedule
73
+ </button>
74
+ </div>
75
+
76
+ <p className="text-sm text-gray-500 mb-6">Find a teammate to swap your upcoming shift.</p>
77
+
78
+ <div className="flex-1 space-y-6">
79
+ <div>
80
+ <EmployeePicker
81
+ label="Select Colleague"
82
+ placeholder="Search employee by name or ID..."
83
+ value={selectedEmployee?.id}
84
+ displayValue={selectedEmployee?.name}
85
+ onChange={(emp) => setSelectedEmployee(emp as any)}
86
+ isRequired
87
+ />
88
+ </div>
89
+
90
+ <div>
91
+ <label className="block text-sm font-bold text-gray-700 mb-2">
92
+ Exchange Note / Reason (Optional)
93
+ </label>
94
+ <textarea
95
+ className="w-full border-gray-200 rounded-lg text-gray-700 focus:ring-blue-500 focus:border-blue-500 resize-none h-24 p-3"
96
+ placeholder="E.g., I have a family event to attend, could you cover my shift?"
97
+ value={note}
98
+ onChange={(e) => setNote(e.target.value)}
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <div className="pt-6 mt-auto border-t border-gray-100">
104
+ <div className="bg-blue-50 border border-blue-100 rounded-xl p-4 flex items-center justify-between">
105
+ <div className="flex items-center gap-3">
106
+ <div className="w-10 h-10 rounded-full bg-white border border-blue-200 flex items-center justify-center text-blue-600 font-bold">
107
+ {selectedEmployee ? selectedEmployee.name.charAt(0) : '?'}
108
+ </div>
109
+ <div>
110
+ <div className="text-sm font-bold text-gray-900">
111
+ {selectedEmployee ? selectedEmployee.name : 'Select a colleague'}
112
+ </div>
113
+ <div className="text-xs text-gray-500">
114
+ {selectedEmployee ? 'to propose exchange' : 'from the list above'}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <Button
119
+ className="h-10 bg-[#0D6EFD] hover:bg-blue-700 text-white font-bold"
120
+ isDisabled={!selectedEmployee}
121
+ onClick={handleConfirm}
122
+ icon={<Send className="w-4 h-4" />}
123
+ >
124
+ Propose Exchange
125
+ </Button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </Modal>
131
+ );
132
+ }
@@ -0,0 +1,183 @@
1
+ import React, { useState, useRef, ChangeEvent } from "react";
2
+ import { Modal, ModalFooter, Button } from "../../index";
3
+ import { bulkPunchAttendance, getEmployeeLookup } from "@apptimate/core-lib";
4
+ import { UploadCloud } from "lucide-react";
5
+ import toast from "react-hot-toast";
6
+ import * as XLSX from "xlsx";
7
+
8
+ interface ImportExcelModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ onSuccess: () => void;
12
+ fetchEmployees?: () => Promise<any>;
13
+ }
14
+
15
+ export function ImportExcelModal({ isOpen, onClose, onSuccess, fetchEmployees }: ImportExcelModalProps) {
16
+ const [file, setFile] = useState<File | null>(null);
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+ const fileInputRef = useRef<HTMLInputElement>(null);
19
+
20
+ const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
21
+ if (e.target.files && e.target.files[0]) {
22
+ setFile(e.target.files[0]);
23
+ }
24
+ };
25
+
26
+ const handleRemoveFile = () => {
27
+ setFile(null);
28
+ if (fileInputRef.current) {
29
+ fileInputRef.current.value = "";
30
+ }
31
+ };
32
+
33
+ const parseExcelAndSubmit = async () => {
34
+ if (!file) {
35
+ toast.error("Please select a file to import");
36
+ return;
37
+ }
38
+
39
+ try {
40
+ setIsSubmitting(true);
41
+
42
+ // Fetch employees to map employee number to employee ID if needed
43
+ const fetcher = fetchEmployees || getEmployeeLookup;
44
+ const empRes = await fetcher();
45
+ const employees = empRes.result || [];
46
+ const empMap = new Map();
47
+ employees.forEach((emp: any) => {
48
+ empMap.set(String(emp.employee_code), emp.id);
49
+ empMap.set(String(emp.id), emp.id); // Also map ID directly just in case
50
+ });
51
+
52
+ const reader = new FileReader();
53
+ reader.onload = async (e) => {
54
+ try {
55
+ const data = e.target?.result;
56
+ const workbook = XLSX.read(data, { type: "array" });
57
+ const sheetName = workbook.SheetNames[0];
58
+ const worksheet = workbook.Sheets[sheetName];
59
+ const json: any[] = XLSX.utils.sheet_to_json(worksheet);
60
+
61
+ const punchesToInsert: any[] = [];
62
+
63
+ json.forEach((row, index) => {
64
+ const empRef = String(row["Employee ID"] || row["Employee Code"] || row["EmployeeId"]);
65
+ const empId = empMap.get(empRef);
66
+
67
+ if (!empId) {
68
+ console.warn(`Row ${index + 1}: Employee not found for ref ${empRef}`);
69
+ return; // Skip row if employee not found
70
+ }
71
+
72
+ const dateStr = row["Date"];
73
+ const punchTimeStr = row["Punch Time"] || row["Time"];
74
+ const typeStr = String(row["Type"] || "I").toUpperCase();
75
+
76
+ if (dateStr && punchTimeStr) {
77
+ // Convert excel date if it's a number
78
+ let formattedDate = dateStr;
79
+ if (typeof dateStr === 'number') {
80
+ const dateObj = new Date(Math.round((dateStr - 25569) * 86400 * 1000));
81
+ formattedDate = dateObj.toISOString().split("T")[0];
82
+ }
83
+
84
+ // Combine Date and Time
85
+ const punchAt = `${formattedDate} ${punchTimeStr}`;
86
+
87
+ punchesToInsert.push({
88
+ employee_id: empId,
89
+ punch_at: punchAt,
90
+ type: typeStr === "O" || typeStr === "OUT" ? "O" : "I",
91
+ source: "Excel Import",
92
+ });
93
+ }
94
+ });
95
+
96
+ if (punchesToInsert.length === 0) {
97
+ toast.error("No valid punches found in the file.");
98
+ setIsSubmitting(false);
99
+ return;
100
+ }
101
+
102
+ const res = await bulkPunchAttendance({ punches: punchesToInsert });
103
+
104
+ if (res.is_success) {
105
+ toast.success(`Successfully imported ${punchesToInsert.length} punches.`);
106
+ onSuccess();
107
+ onClose();
108
+ } else {
109
+ toast.error(res.message || "Failed to import attendance.");
110
+ }
111
+ } catch (err: any) {
112
+ toast.error("Error parsing Excel file: " + err.message);
113
+ } finally {
114
+ setIsSubmitting(false);
115
+ }
116
+ };
117
+ reader.readAsArrayBuffer(file);
118
+ } catch (e: any) {
119
+ toast.error(e.message || "An error occurred");
120
+ setIsSubmitting(false);
121
+ }
122
+ };
123
+
124
+ return (
125
+ <Modal isOpen={isOpen} onClose={onClose} title="Import Attendance (Excel/CSV)" size="md">
126
+ <div className="space-y-4 py-4">
127
+
128
+ <div
129
+ className="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col items-center justify-center bg-gray-50 text-center cursor-pointer hover:bg-gray-100 transition-colors"
130
+ onClick={() => fileInputRef.current?.click()}
131
+ >
132
+ <input
133
+ type="file"
134
+ ref={fileInputRef}
135
+ className="hidden"
136
+ accept=".xlsx, .xls, .csv"
137
+ onChange={handleFileChange}
138
+ />
139
+ <UploadCloud className="w-10 h-10 text-gray-400 mb-3" />
140
+ {file ? (
141
+ <div>
142
+ <p className="text-sm font-semibold text-gray-800">{file.name}</p>
143
+ <p className="text-xs text-gray-500 mt-1">{(file.size / 1024).toFixed(2)} KB</p>
144
+ </div>
145
+ ) : (
146
+ <div>
147
+ <p className="text-sm font-semibold text-gray-800">Click to upload or drag and drop</p>
148
+ <p className="text-xs text-gray-500 mt-1">XLSX, XLS, or CSV</p>
149
+ </div>
150
+ )}
151
+ </div>
152
+
153
+ {file && (
154
+ <div className="flex justify-end">
155
+ <button type="button" onClick={handleRemoveFile} className="text-xs text-danger-500 hover:underline">
156
+ Remove file
157
+ </button>
158
+ </div>
159
+ )}
160
+
161
+ <div className="bg-primary-50 p-4 rounded-xl border border-primary-100">
162
+ <h4 className="text-sm font-bold text-primary-800 mb-2">Required Columns</h4>
163
+ <ul className="text-xs text-primary-700 space-y-1 list-disc list-inside">
164
+ <li><span className="font-semibold">Employee ID</span> (or Employee Code)</li>
165
+ <li><span className="font-semibold">Date</span> (YYYY-MM-DD)</li>
166
+ <li><span className="font-semibold">Punch Time</span> (HH:MM:SS)</li>
167
+ <li><span className="font-semibold">Type</span> (I for In, O for Out)</li>
168
+ </ul>
169
+ </div>
170
+
171
+ </div>
172
+
173
+ <ModalFooter>
174
+ <Button variant="flat" color="secondary" onClick={onClose} isDisabled={isSubmitting}>
175
+ Cancel
176
+ </Button>
177
+ <Button color="primary" onClick={parseExcelAndSubmit} isLoading={isSubmitting}>
178
+ Upload & Import
179
+ </Button>
180
+ </ModalFooter>
181
+ </Modal>
182
+ );
183
+ }
@@ -0,0 +1,139 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Modal, ModalFooter } from "../../base-components/Modal";
3
+ import { Button } from "../../base-components/Button";
4
+ import { Input } from "../../base-components/Input";
5
+ import { Select } from "../../base-components/Select";
6
+ import { punchAttendance, getEmployeeLookup } from "@apptimate/core-lib";
7
+ import toast from "react-hot-toast";
8
+
9
+ interface ManualPunchModalProps {
10
+ isOpen: boolean;
11
+ onClose: () => void;
12
+ onSuccess: () => void;
13
+ fetchEmployees?: () => Promise<any>;
14
+ }
15
+
16
+ export function ManualPunchModal({ isOpen, onClose, onSuccess, fetchEmployees }: ManualPunchModalProps) {
17
+ const [employees, setEmployees] = useState<any[]>([]);
18
+ const [isSubmitting, setIsSubmitting] = useState(false);
19
+ const [formData, setFormData] = useState({
20
+ employee_id: "",
21
+ punch_date: new Date().toISOString().split("T")[0],
22
+ punch_time: new Date().toTimeString().substring(0, 5),
23
+ type: "I",
24
+ });
25
+
26
+ useEffect(() => {
27
+ if (isOpen) {
28
+ loadEmployees();
29
+ // Reset form
30
+ setFormData({
31
+ employee_id: "",
32
+ punch_date: new Date().toISOString().split("T")[0],
33
+ punch_time: new Date().toTimeString().substring(0, 5),
34
+ type: "I",
35
+ });
36
+ }
37
+ }, [isOpen]);
38
+
39
+ const loadEmployees = async () => {
40
+ try {
41
+ const fetcher = fetchEmployees || getEmployeeLookup;
42
+ const res = await fetcher();
43
+ if (res.is_success && res.result) {
44
+ setEmployees(res.result);
45
+ }
46
+ } catch (e: any) {
47
+ console.error(e);
48
+ }
49
+ };
50
+
51
+ const handleSubmit = async () => {
52
+ if (!formData.employee_id || !formData.punch_date || !formData.punch_time) {
53
+ toast.error("Please fill all required fields");
54
+ return;
55
+ }
56
+
57
+ try {
58
+ setIsSubmitting(true);
59
+ const punchAt = `${formData.punch_date} ${formData.punch_time}:00`;
60
+
61
+ const res = await punchAttendance({
62
+ employee_id: formData.employee_id,
63
+ punch_at: punchAt,
64
+ type: formData.type,
65
+ source: "Manual",
66
+ });
67
+
68
+ if (res.is_success) {
69
+ toast.success("Punch logged successfully");
70
+ onSuccess();
71
+ onClose();
72
+ } else {
73
+ toast.error(res.message || "Failed to log punch");
74
+ }
75
+ } catch (e: any) {
76
+ toast.error(e.message || "An error occurred");
77
+ } finally {
78
+ setIsSubmitting(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <Modal isOpen={isOpen} onClose={onClose} title="Manual Attendance Log" size="md">
84
+ <div className="space-y-4 py-2">
85
+ <div>
86
+ <label className="block text-sm font-medium text-gray-700 mb-1">Employee <span className="text-danger-500">*</span></label>
87
+ <Select
88
+ value={formData.employee_id}
89
+ onChange={(e) => setFormData({ ...formData, employee_id: e.target.value })}
90
+ options={employees.map((emp) => ({
91
+ value: String(emp.id),
92
+ label: `${emp.first_name} ${emp.last_name} (${emp.employee_code || ''})`
93
+ }))}
94
+ />
95
+ </div>
96
+
97
+ <div className="grid grid-cols-2 gap-4">
98
+ <div>
99
+ <label className="block text-sm font-medium text-gray-700 mb-1">Date <span className="text-danger-500">*</span></label>
100
+ <Input
101
+ type="date"
102
+ value={formData.punch_date}
103
+ onChange={(e) => setFormData({ ...formData, punch_date: e.target.value })}
104
+ />
105
+ </div>
106
+ <div>
107
+ <label className="block text-sm font-medium text-gray-700 mb-1">Time <span className="text-danger-500">*</span></label>
108
+ <Input
109
+ type="time"
110
+ value={formData.punch_time}
111
+ onChange={(e) => setFormData({ ...formData, punch_time: e.target.value })}
112
+ />
113
+ </div>
114
+ </div>
115
+
116
+ <div>
117
+ <label className="block text-sm font-medium text-gray-700 mb-1">Punch Type <span className="text-danger-500">*</span></label>
118
+ <Select
119
+ value={formData.type}
120
+ onChange={(e) => setFormData({ ...formData, type: e.target.value })}
121
+ options={[
122
+ { value: "I", label: "In (Check-In)" },
123
+ { value: "O", label: "Out (Check-Out)" }
124
+ ]}
125
+ />
126
+ </div>
127
+ </div>
128
+
129
+ <ModalFooter>
130
+ <Button variant="flat" color="secondary" onClick={onClose} isDisabled={isSubmitting}>
131
+ Cancel
132
+ </Button>
133
+ <Button color="primary" onClick={handleSubmit} isLoading={isSubmitting}>
134
+ Save Log
135
+ </Button>
136
+ </ModalFooter>
137
+ </Modal>
138
+ );
139
+ }