@apptimate/ui 5.2.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/pickers/EmployeePicker.tsx +120 -0
- package/src/common-components/pickers/EntityPickerModal.tsx +6 -4
- package/src/index.tsx +19 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDown,
|
|
6
|
+
CalendarClock,
|
|
7
|
+
FileCheck2,
|
|
8
|
+
Palmtree,
|
|
9
|
+
Clock,
|
|
10
|
+
AlertCircle,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import { format } from "date-fns";
|
|
13
|
+
import { getPendingActions } from "@apptimate/core-lib";
|
|
14
|
+
import toast from "react-hot-toast";
|
|
15
|
+
|
|
16
|
+
interface PendingActionsPanelProps {
|
|
17
|
+
employeeId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Section {
|
|
21
|
+
key: string;
|
|
22
|
+
title: string;
|
|
23
|
+
icon: React.ElementType;
|
|
24
|
+
iconBg: string;
|
|
25
|
+
iconColor: string;
|
|
26
|
+
count?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function PendingActionsPanel({ employeeId }: PendingActionsPanelProps) {
|
|
30
|
+
const [data, setData] = useState<any>(null);
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
33
|
+
leaves: true,
|
|
34
|
+
regularisation: false,
|
|
35
|
+
holidays: true,
|
|
36
|
+
overtime: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const fetchData = useCallback(async () => {
|
|
40
|
+
try {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
const res = await getPendingActions(employeeId);
|
|
43
|
+
if (res.is_success && res.result) {
|
|
44
|
+
setData(res.result);
|
|
45
|
+
}
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
toast.error(e.message || "Failed to load pending actions");
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, [employeeId]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
fetchData();
|
|
55
|
+
}, [fetchData]);
|
|
56
|
+
|
|
57
|
+
const toggleSection = (key: string) => {
|
|
58
|
+
setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sections: Section[] = [
|
|
62
|
+
{
|
|
63
|
+
key: "leaves",
|
|
64
|
+
title: "Pending Leaves",
|
|
65
|
+
icon: CalendarClock,
|
|
66
|
+
iconBg: "bg-amber-50",
|
|
67
|
+
iconColor: "text-amber-600",
|
|
68
|
+
count: data?.pending_leaves?.length || 0,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "regularisation",
|
|
72
|
+
title: "Regularisation",
|
|
73
|
+
icon: FileCheck2,
|
|
74
|
+
iconBg: "bg-blue-50",
|
|
75
|
+
iconColor: "text-blue-600",
|
|
76
|
+
count: data?.pending_regularisation?.length || 0,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "holidays",
|
|
80
|
+
title: "Upcoming Holidays",
|
|
81
|
+
icon: Palmtree,
|
|
82
|
+
iconBg: "bg-indigo-50",
|
|
83
|
+
iconColor: "text-indigo-600",
|
|
84
|
+
count: data?.upcoming_holidays?.length || 0,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "overtime",
|
|
88
|
+
title: "OT Summary",
|
|
89
|
+
icon: Clock,
|
|
90
|
+
iconBg: "bg-purple-50",
|
|
91
|
+
iconColor: "text-purple-600",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const getStatusBadgeClass = (status: string) => {
|
|
96
|
+
switch (status?.toLowerCase()) {
|
|
97
|
+
case "approved":
|
|
98
|
+
return "bg-emerald-50 text-emerald-700 border-emerald-200";
|
|
99
|
+
case "pending":
|
|
100
|
+
return "bg-amber-50 text-amber-700 border-amber-200";
|
|
101
|
+
case "rejected":
|
|
102
|
+
return "bg-red-50 text-red-700 border-red-200";
|
|
103
|
+
default:
|
|
104
|
+
return "bg-gray-50 text-gray-700 border-gray-200";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (loading) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="w-80 flex-shrink-0 bg-white rounded-[16px] border border-gray-200 shadow-sm p-6 flex items-center justify-center">
|
|
111
|
+
<div className="w-6 h-6 border-3 border-gray-200 border-t-primary-600 rounded-full animate-spin" />
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="w-80 flex-shrink-0 bg-white rounded-[16px] border border-gray-200 shadow-sm overflow-hidden flex flex-col max-h-[calc(100vh-140px)]">
|
|
118
|
+
{/* Panel Header */}
|
|
119
|
+
<div className="p-4 border-b border-gray-100 bg-gradient-to-r from-slate-50 to-white">
|
|
120
|
+
<div className="flex items-center gap-2.5">
|
|
121
|
+
<div className="w-8 h-8 rounded-lg bg-primary-50 text-primary-600 flex items-center justify-center">
|
|
122
|
+
<AlertCircle size={16} />
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<h3 className="text-sm font-bold text-gray-900">Pending Actions</h3>
|
|
126
|
+
<p className="text-[11px] text-gray-500">Quick overview of your items</p>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Scrollable content */}
|
|
132
|
+
<div className="flex-1 overflow-y-auto">
|
|
133
|
+
{sections.map((section) => {
|
|
134
|
+
const Icon = section.icon;
|
|
135
|
+
const isExpanded = expandedSections[section.key];
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div key={section.key} className="border-b border-gray-100 last:border-b-0">
|
|
139
|
+
{/* Section Header */}
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => toggleSection(section.key)}
|
|
142
|
+
className="w-full flex items-center justify-between p-3.5 hover:bg-gray-50/60 transition-colors"
|
|
143
|
+
>
|
|
144
|
+
<div className="flex items-center gap-2.5">
|
|
145
|
+
<div className={`w-7 h-7 rounded-lg ${section.iconBg} flex items-center justify-center`}>
|
|
146
|
+
<Icon size={14} className={section.iconColor} />
|
|
147
|
+
</div>
|
|
148
|
+
<span className="text-[13px] font-semibold text-gray-800">{section.title}</span>
|
|
149
|
+
{section.count !== undefined && section.count > 0 && (
|
|
150
|
+
<span className="text-[10px] font-bold bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full min-w-[20px] text-center">
|
|
151
|
+
{section.count}
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
<ChevronDown
|
|
156
|
+
size={16}
|
|
157
|
+
className={`text-gray-400 transition-transform duration-300 ${isExpanded ? "rotate-180" : ""}`}
|
|
158
|
+
/>
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{/* Section Content */}
|
|
162
|
+
<div
|
|
163
|
+
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
|
164
|
+
isExpanded ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<div className="px-3.5 pb-3.5 space-y-2">
|
|
168
|
+
{/* Pending Leaves */}
|
|
169
|
+
{section.key === "leaves" && (
|
|
170
|
+
<>
|
|
171
|
+
{data?.pending_leaves && data.pending_leaves.length > 0 ? (
|
|
172
|
+
data.pending_leaves.map((leave: any, idx: number) => (
|
|
173
|
+
<div
|
|
174
|
+
key={idx}
|
|
175
|
+
className="p-2.5 rounded-xl border border-gray-100 bg-gray-50/50 hover:bg-gray-50 transition-colors"
|
|
176
|
+
>
|
|
177
|
+
<div className="flex items-start justify-between gap-2 mb-1">
|
|
178
|
+
<span className="text-[12.5px] font-semibold text-gray-800 truncate">
|
|
179
|
+
{leave.leave_type?.name || "Leave"}
|
|
180
|
+
</span>
|
|
181
|
+
<span
|
|
182
|
+
className={`text-[10px] font-bold px-1.5 py-0.5 rounded-full border whitespace-nowrap ${getStatusBadgeClass(
|
|
183
|
+
leave.status
|
|
184
|
+
)}`}
|
|
185
|
+
>
|
|
186
|
+
{leave.status}
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
<p className="text-[11px] text-gray-500">
|
|
190
|
+
{leave.from_date
|
|
191
|
+
? format(new Date(leave.from_date), "MMM d")
|
|
192
|
+
: ""}{" "}
|
|
193
|
+
–{" "}
|
|
194
|
+
{leave.to_date
|
|
195
|
+
? format(new Date(leave.to_date), "MMM d, yyyy")
|
|
196
|
+
: ""}
|
|
197
|
+
</p>
|
|
198
|
+
{leave.days && (
|
|
199
|
+
<p className="text-[10.5px] text-gray-400 mt-0.5">
|
|
200
|
+
{leave.days} day{leave.days > 1 ? "s" : ""}
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
))
|
|
205
|
+
) : (
|
|
206
|
+
<p className="text-[11.5px] text-gray-400 text-center py-3">
|
|
207
|
+
No pending leave applications
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{/* Regularisation */}
|
|
214
|
+
{section.key === "regularisation" && (
|
|
215
|
+
<>
|
|
216
|
+
{data?.pending_regularisation && data.pending_regularisation.length > 0 ? (
|
|
217
|
+
data.pending_regularisation.map((req: any, idx: number) => (
|
|
218
|
+
<div
|
|
219
|
+
key={idx}
|
|
220
|
+
className="p-2.5 rounded-xl border border-gray-100 bg-gray-50/50 hover:bg-gray-50 transition-colors"
|
|
221
|
+
>
|
|
222
|
+
<div className="flex items-start justify-between gap-2 mb-1">
|
|
223
|
+
<span className="text-[12.5px] font-semibold text-gray-800">
|
|
224
|
+
{req.date
|
|
225
|
+
? format(new Date(req.date), "MMM d, yyyy")
|
|
226
|
+
: "Regularisation"}
|
|
227
|
+
</span>
|
|
228
|
+
<span
|
|
229
|
+
className={`text-[10px] font-bold px-1.5 py-0.5 rounded-full border whitespace-nowrap ${getStatusBadgeClass(
|
|
230
|
+
req.status
|
|
231
|
+
)}`}
|
|
232
|
+
>
|
|
233
|
+
{req.status}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
{req.reason && (
|
|
237
|
+
<p className="text-[11px] text-gray-500 line-clamp-2">{req.reason}</p>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
))
|
|
241
|
+
) : (
|
|
242
|
+
<p className="text-[11.5px] text-gray-400 text-center py-3">
|
|
243
|
+
No pending regularisation requests
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Upcoming Holidays */}
|
|
250
|
+
{section.key === "holidays" && (
|
|
251
|
+
<>
|
|
252
|
+
{data?.upcoming_holidays && data.upcoming_holidays.length > 0 ? (
|
|
253
|
+
data.upcoming_holidays.slice(0, 3).map((holiday: any, idx: number) => (
|
|
254
|
+
<div
|
|
255
|
+
key={idx}
|
|
256
|
+
className="flex items-center gap-2.5 p-2.5 rounded-xl border border-gray-100 bg-gray-50/50 hover:bg-gray-50 transition-colors"
|
|
257
|
+
>
|
|
258
|
+
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex flex-col items-center justify-center flex-shrink-0">
|
|
259
|
+
<span className="text-[10px] font-bold text-indigo-500 uppercase leading-tight">
|
|
260
|
+
{holiday.date
|
|
261
|
+
? format(new Date(holiday.date), "MMM")
|
|
262
|
+
: ""}
|
|
263
|
+
</span>
|
|
264
|
+
<span className="text-[14px] font-bold text-indigo-700 leading-tight">
|
|
265
|
+
{holiday.date
|
|
266
|
+
? format(new Date(holiday.date), "d")
|
|
267
|
+
: ""}
|
|
268
|
+
</span>
|
|
269
|
+
</div>
|
|
270
|
+
<div className="min-w-0 flex-1">
|
|
271
|
+
<p className="text-[12.5px] font-semibold text-gray-800 truncate">
|
|
272
|
+
{holiday.name}
|
|
273
|
+
</p>
|
|
274
|
+
<p className="text-[10.5px] text-gray-400">
|
|
275
|
+
{holiday.date
|
|
276
|
+
? format(new Date(holiday.date), "EEEE")
|
|
277
|
+
: ""}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
))
|
|
282
|
+
) : (
|
|
283
|
+
<p className="text-[11.5px] text-gray-400 text-center py-3">
|
|
284
|
+
No upcoming holidays
|
|
285
|
+
</p>
|
|
286
|
+
)}
|
|
287
|
+
</>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* OT Summary */}
|
|
291
|
+
{section.key === "overtime" && (
|
|
292
|
+
<div className="grid grid-cols-2 gap-2">
|
|
293
|
+
<div className="p-3 rounded-xl bg-purple-50/60 border border-purple-100 text-center">
|
|
294
|
+
<p className="text-[18px] font-bold text-purple-700">
|
|
295
|
+
{data?.ot_summary?.total_hours ?? 0}
|
|
296
|
+
</p>
|
|
297
|
+
<p className="text-[10px] font-medium text-purple-500 uppercase tracking-wider">
|
|
298
|
+
OT Hours
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="p-3 rounded-xl bg-emerald-50/60 border border-emerald-100 text-center">
|
|
302
|
+
<p className="text-[18px] font-bold text-emerald-700">
|
|
303
|
+
{data?.ot_summary?.total_amount
|
|
304
|
+
? `${data.ot_summary.total_amount.toLocaleString()}`
|
|
305
|
+
: "0"}
|
|
306
|
+
</p>
|
|
307
|
+
<p className="text-[10px] font-medium text-emerald-500 uppercase tracking-wider">
|
|
308
|
+
OT Amount
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|