@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.
@@ -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
+ }