@apptimate/ui 6.9.0 → 7.1.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.
@@ -14,11 +14,12 @@ interface BulkAttendanceModalProps {
14
14
  onClose: () => void;
15
15
  onSuccess: () => void;
16
16
  fetchProjects?: () => Promise<any>;
17
+ fetchAccounts?: (type?: string) => Promise<any>;
17
18
  initialProjectId?: string;
18
19
  isFixedProject?: boolean;
19
20
  }
20
21
 
21
- export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects, initialProjectId, isFixedProject }: BulkAttendanceModalProps) {
22
+ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects, fetchAccounts, initialProjectId, isFixedProject }: BulkAttendanceModalProps) {
22
23
  const [projects, setProjects] = useState<any[]>([]);
23
24
  const [projectId, setProjectId] = useState<string>(initialProjectId || "");
24
25
  const [attendanceDate, setAttendanceDate] = useState<string>(new Date().toISOString().split("T")[0]);
@@ -26,28 +27,45 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
26
27
  const [globalEmployees, setGlobalEmployees] = useState<any[]>([]);
27
28
  const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
28
29
  const [isSubmitting, setIsSubmitting] = useState(false);
30
+ const [accounts, setAccounts] = useState<any[]>([]);
31
+ const [paymentAccountId, setPaymentAccountId] = useState<string>("");
29
32
 
30
- // Time entries state: Record<employee_id, { in: string, out: string, break: number, isPresent: boolean }>
31
- const [timeEntries, setTimeEntries] = useState<Record<string, { in: string, out: string, break: number, isPresent: boolean }>>({});
33
+ // Time entries state: Record<employee_id, { in: string, out: string, break: number, isPresent: boolean, salaryAdvance: number }>
34
+ const [timeEntries, setTimeEntries] = useState<Record<string, { in: string, out: string, break: number, isPresent: boolean, salaryAdvance: number }>>({});
32
35
 
33
36
  useEffect(() => {
34
37
  if (isOpen) {
35
38
  loadProjects();
36
39
  loadGlobalEmployees();
40
+ if (fetchAccounts) {
41
+ fetchAccounts("asset").then((res) => {
42
+ if (res.is_success && res.result) {
43
+ setAccounts(res.result);
44
+ }
45
+ });
46
+ }
37
47
  setAttendanceDate(new Date().toISOString().split("T")[0]);
38
48
  setProjectId(initialProjectId || "");
39
- setEmployees([]);
49
+ setPaymentAccountId("");
40
50
  setTimeEntries({});
51
+
52
+ // If initialProjectId is already set, the second useEffect won't trigger because projectId hasn't changed.
53
+ // So we force a load if we already have the ID.
54
+ if (initialProjectId) {
55
+ // loadEmployees will be triggered by the second useEffect if projectId changes.
56
+ // If it doesn't change, we need to call it here.
57
+ // We will just let the second useEffect handle it by adding isOpen to its dependencies.
58
+ }
41
59
  }
42
60
  }, [isOpen, initialProjectId]);
43
61
 
44
62
  useEffect(() => {
45
- if (projectId && attendanceDate) {
63
+ if (isOpen && projectId && attendanceDate) {
46
64
  loadEmployees();
47
- } else {
65
+ } else if (!isOpen) {
48
66
  setEmployees([]);
49
67
  }
50
- }, [projectId, attendanceDate]);
68
+ }, [projectId, attendanceDate, isOpen]);
51
69
 
52
70
  const loadGlobalEmployees = async () => {
53
71
  try {
@@ -83,9 +101,23 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
83
101
  setEmployees(res.result);
84
102
 
85
103
  // Initialize time entries for these employees
86
- const newEntries: Record<string, { in: string, out: string, break: number, isPresent: boolean }> = {};
104
+ const newEntries: Record<string, { in: string, out: string, break: number, isPresent: boolean, salaryAdvance: number }> = {};
87
105
  res.result.forEach((emp: any) => {
88
- newEntries[emp.id] = { in: "09:00", out: "18:00", break: 0, isPresent: true };
106
+ let inTime = "09:00";
107
+ let outTime = "18:00";
108
+ let breakHrs = 0;
109
+ let isPresent = true;
110
+ let salaryAdvance = 0;
111
+
112
+ if (emp.attendance) {
113
+ isPresent = emp.attendance.status === 'Present';
114
+ if (emp.attendance.punch_in_time) inTime = emp.attendance.punch_in_time;
115
+ if (emp.attendance.punch_out_time) outTime = emp.attendance.punch_out_time;
116
+ breakHrs = emp.attendance.break_hours || 0;
117
+ salaryAdvance = emp.attendance.salary_advance || 0;
118
+ }
119
+
120
+ newEntries[emp.id] = { in: inTime, out: outTime, break: breakHrs, isPresent: isPresent, salaryAdvance: salaryAdvance };
89
121
  });
90
122
  setTimeEntries(newEntries);
91
123
  }
@@ -96,7 +128,7 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
96
128
  }
97
129
  };
98
130
 
99
- const handleTimeChange = (empId: string, type: 'in' | 'out' | 'break' | 'isPresent', value: string | number | boolean) => {
131
+ const handleTimeChange = (empId: string, type: 'in' | 'out' | 'break' | 'isPresent' | 'salaryAdvance', value: string | number | boolean) => {
100
132
  setTimeEntries(prev => ({
101
133
  ...prev,
102
134
  [empId]: {
@@ -111,7 +143,7 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
111
143
  setEmployees(prev => [...prev, { id: fakeId, is_manual: true }]);
112
144
  setTimeEntries(prev => ({
113
145
  ...prev,
114
- [fakeId]: { in: "09:00", out: "18:00", break: 0, isPresent: true }
146
+ [fakeId]: { in: "09:00", out: "18:00", break: 0, isPresent: true, salaryAdvance: 0 }
115
147
  }));
116
148
  };
117
149
 
@@ -157,7 +189,7 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
157
189
  employees.forEach(emp => {
158
190
  const entry = timeEntries[emp.id];
159
191
 
160
- if (!entry || !entry.isPresent) return;
192
+ if (!entry) return;
161
193
 
162
194
  if (String(emp.id).startsWith('wf-')) {
163
195
  // It's a workforce gang worker
@@ -165,19 +197,45 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
165
197
  const workforceId = parts[1];
166
198
  const memberIdentifier = parts[2] || null;
167
199
 
168
- if (entry.in || entry.out) {
200
+ if (entry.isPresent) {
201
+ if (entry.in || entry.out) {
202
+ workforcePunches.push({
203
+ workforce_id: Number(workforceId),
204
+ project_id: Number(projectId),
205
+ date: attendanceDate,
206
+ member_identifier: memberIdentifier,
207
+ punch_in_time: entry.in || null,
208
+ punch_out_time: entry.out || null,
209
+ break_hours: entry.break || 0,
210
+ salary_advance: entry.salaryAdvance || 0,
211
+ });
212
+ } else if (entry.salaryAdvance > 0) {
213
+ // If they only have a salary advance but no punch
214
+ workforcePunches.push({
215
+ workforce_id: Number(workforceId),
216
+ project_id: Number(projectId),
217
+ date: attendanceDate,
218
+ member_identifier: memberIdentifier,
219
+ salary_advance: entry.salaryAdvance,
220
+ });
221
+ }
222
+ } else {
223
+ // ABSENT
169
224
  workforcePunches.push({
170
225
  workforce_id: Number(workforceId),
171
226
  project_id: Number(projectId),
172
227
  date: attendanceDate,
173
228
  member_identifier: memberIdentifier,
174
- punch_in_time: entry.in || null,
175
- punch_out_time: entry.out || null,
176
- break_hours: entry.break || 0,
229
+ punch_in_time: null,
230
+ punch_out_time: null,
231
+ break_hours: 0,
232
+ salary_advance: entry.salaryAdvance || 0,
177
233
  });
178
234
  }
179
235
  } else {
180
236
  // It's a standard HR employee
237
+ if (!entry.isPresent) return; // Skip absent for HR for now
238
+
181
239
  if (entry.in) {
182
240
  hrPunches.push({
183
241
  employee_id: emp.id,
@@ -191,8 +249,21 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
191
249
  employee_id: emp.id,
192
250
  punch_at: `${attendanceDate} ${entry.out}:00`,
193
251
  type: "O",
194
- source: "Bulk"
252
+ source: "Bulk",
253
+ salary_advance: entry.salaryAdvance || 0
195
254
  });
255
+ } else if (!entry.in && entry.salaryAdvance > 0) {
256
+ // If they only have a salary advance but no punch
257
+ hrPunches.push({
258
+ employee_id: emp.id,
259
+ type: "ADVANCE", // Special type or handled by backend if punch_at is empty
260
+ source: "Bulk",
261
+ salary_advance: entry.salaryAdvance,
262
+ date: attendanceDate
263
+ });
264
+ } else if (hrPunches.length > 0 && entry.salaryAdvance > 0) {
265
+ // Add salary advance to the first punch we pushed for this employee
266
+ hrPunches[hrPunches.length - 1].salary_advance = entry.salaryAdvance;
196
267
  }
197
268
  }
198
269
  });
@@ -207,7 +278,10 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
207
278
 
208
279
  // Log HR punches
209
280
  if (hrPunches.length > 0) {
210
- const hrRes = await bulkPunchAttendance({ punches: hrPunches });
281
+ const hrRes = await bulkPunchAttendance({
282
+ punches: hrPunches,
283
+ payment_account_id: paymentAccountId ? Number(paymentAccountId) : undefined
284
+ });
211
285
  if (!hrRes.is_success) {
212
286
  toast.error(hrRes.message || "Failed to log HR attendance");
213
287
  hasError = true;
@@ -218,9 +292,16 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
218
292
  if (workforcePunches.length > 0) {
219
293
  // use sendRequest from core-lib which is imported at the top of this file
220
294
  const { sendRequest } = require("@apptimate/core-lib");
221
- const wfRes = await sendRequest({ url: "/api/construction/workforce-attendance/bulk-punch", method: "POST", data: { punches: workforcePunches } });
222
- if (!(wfRes as any).is_success) {
223
- toast.error((wfRes as any).message || "Failed to log Workforce attendance");
295
+ const wfRes = await sendRequest({
296
+ url: "/api/construction/workforce-attendance/bulk-punch",
297
+ method: "POST",
298
+ data: {
299
+ punches: workforcePunches,
300
+ payment_account_id: paymentAccountId ? Number(paymentAccountId) : undefined
301
+ }
302
+ });
303
+ if (!wfRes.responseData?.is_success) {
304
+ toast.error(wfRes.responseData?.message || "Failed to log Workforce attendance");
224
305
  hasError = true;
225
306
  }
226
307
  }
@@ -242,7 +323,7 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
242
323
  return (
243
324
  <Modal isOpen={isOpen} onClose={onClose} title="Bulk Attendance Log" size="2xl">
244
325
  <div className="space-y-4 py-2">
245
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
326
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
246
327
  <div>
247
328
  <label className="block text-sm font-medium text-gray-700 mb-1">Date <span className="text-danger-500">*</span></label>
248
329
  <Input
@@ -254,20 +335,28 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
254
335
  <div>
255
336
  <label className="block text-sm font-medium text-gray-700 mb-1">Project <span className="text-danger-500">*</span></label>
256
337
  <Select
338
+ options={projects.map((p) => ({ value: String(p.id), label: p.name }))}
257
339
  value={projectId}
258
340
  onChange={(e) => setProjectId(e.target.value)}
259
341
  disabled={isFixedProject}
260
- options={[
261
- { value: "", label: "Select Project..." },
262
- ...projects.map((p) => ({
263
- value: String(p.id),
264
- label: p.name
265
- }))
266
- ]}
267
342
  />
268
343
  </div>
344
+ {fetchAccounts && (
345
+ <div>
346
+ <label className="block text-sm font-medium text-gray-700 mb-1">Payment Account <span className="text-gray-400 font-normal text-xs">(Salary Advances)</span></label>
347
+ <Select
348
+ options={[
349
+ { value: "", label: "Default mapping" },
350
+ ...accounts.map((a) => ({ value: String(a.id), label: `${a.code} - ${a.name}` }))
351
+ ]}
352
+ value={paymentAccountId}
353
+ onChange={(e) => setPaymentAccountId(e.target.value)}
354
+ />
355
+ </div>
356
+ )}
269
357
  </div>
270
358
 
359
+
271
360
  {projectId && attendanceDate && (
272
361
  <div className="mt-6 border border-gray-100 rounded-xl overflow-hidden bg-white shadow-sm">
273
362
  <Table>
@@ -277,7 +366,8 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
277
366
  <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-24">Present</TCell>
278
367
  <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-32">First In</TCell>
279
368
  <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-32">Last Out</TCell>
280
- <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-32">Break (hrs)</TCell>
369
+ <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-24">Break (hrs)</TCell>
370
+ <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0 w-48">Salary Advance</TCell>
281
371
  </TRow>
282
372
  </THeader>
283
373
  <TBody>
@@ -353,6 +443,41 @@ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects,
353
443
  title={!String(emp.id).startsWith('wf-') ? "Break hours are only applicable to Construction Workforce" : ""}
354
444
  />
355
445
  </TCell>
446
+ <TCell label="Salary Advance">
447
+ {emp.is_gang ? (
448
+ <div className="flex items-center h-9">
449
+ <span className="text-gray-400">-</span>
450
+ </div>
451
+ ) : (
452
+ <div className="flex items-center gap-2">
453
+ <Input
454
+ type="number"
455
+ step="0.01"
456
+ min="0"
457
+ value={timeEntries[emp.id]?.salaryAdvance || 0}
458
+ onChange={(e) => handleTimeChange(emp.id, 'salaryAdvance', Number(e.target.value))}
459
+ className="h-9 w-24 shrink-0"
460
+ disabled={!timeEntries[emp.id]?.isPresent}
461
+ />
462
+ {emp.balance_due !== undefined && (
463
+ <div className="text-[11px] text-gray-500 font-medium whitespace-nowrap">
464
+ {timeEntries[emp.id]?.salaryAdvance > 0 ? (
465
+ <>
466
+ Owed: {Number(emp.balance_due) + Number(emp.attendance?.salary_advance || 0)} - {timeEntries[emp.id].salaryAdvance} = <strong className="text-gray-700">{Number(emp.balance_due) + Number(emp.attendance?.salary_advance || 0) - timeEntries[emp.id].salaryAdvance}</strong>
467
+ </>
468
+ ) : (
469
+ <>Owed: {Number(emp.balance_due) + Number(emp.attendance?.salary_advance || 0)}</>
470
+ )}
471
+ {timeEntries[emp.id]?.isPresent && (
472
+ <span className="block text-[9.5px] text-blue-500/90 mt-0.5">
473
+ + Today's wage
474
+ </span>
475
+ )}
476
+ </div>
477
+ )}
478
+ </div>
479
+ )}
480
+ </TCell>
356
481
  </TRow>
357
482
  ))
358
483
  )}
@@ -14,6 +14,7 @@ interface DailySchedulePanelProps {
14
14
 
15
15
  export default function DailySchedulePanel({ day, events, onCancelClick, onExchangeClick, onCancelExchangeRequest, onClose }: DailySchedulePanelProps) {
16
16
  const shifts = events.filter(e => e.type === 'shift' && e.segmentsInfo);
17
+ const projectAttendances = events.filter(e => e.type === 'project_attendance');
17
18
 
18
19
  return (
19
20
  <div className="absolute inset-0 z-30 lg:static lg:z-auto w-full lg:w-80 flex-shrink-0 bg-white rounded-[16px] border border-gray-200 overflow-hidden flex flex-col h-[calc(100vh-140px)] shadow-2xl lg:shadow-none">
@@ -34,19 +35,79 @@ export default function DailySchedulePanel({ day, events, onCancelClick, onExcha
34
35
  </div>
35
36
 
36
37
  <div className="flex-1 overflow-y-auto p-4 space-y-4">
38
+ {projectAttendances.length > 0 && (
39
+ <div className="space-y-3">
40
+ <h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider">Project Summary</h3>
41
+ {projectAttendances.map((pa, idx) => (
42
+ <div key={idx} className="bg-emerald-50 border border-emerald-100 rounded-xl overflow-hidden shadow-sm">
43
+ <div className="p-4 border-b border-emerald-100/50">
44
+ <div className="font-bold text-emerald-900 mb-2">{pa.name}</div>
45
+ <div className="flex justify-between items-center text-sm font-medium">
46
+ <span className="text-emerald-700 flex items-center gap-1.5">
47
+ <div className="w-2 h-2 rounded-full bg-emerald-500"></div>
48
+ {pa.present} Present
49
+ </span>
50
+ <span className="text-red-700 flex items-center gap-1.5">
51
+ <div className="w-2 h-2 rounded-full bg-red-500"></div>
52
+ {pa.absent} Absent
53
+ </span>
54
+ </div>
55
+ </div>
56
+ {pa.workers && pa.workers.length > 0 && (
57
+ <div className="bg-white/60 p-3 space-y-2">
58
+ {pa.workers.map((worker: any, wIdx: number) => (
59
+ <div key={wIdx} className="flex flex-col border-b border-gray-100 last:border-0 pb-2 last:pb-0">
60
+ <div className="flex justify-between items-center text-xs">
61
+ <span className="font-medium text-gray-700 truncate pr-2">{worker.name}</span>
62
+ <span className={`px-2 py-0.5 rounded-full font-semibold shrink-0 text-[10px] ${
63
+ worker.status === 'Present'
64
+ ? 'bg-emerald-100 text-emerald-700'
65
+ : 'bg-red-100 text-red-700'
66
+ }`}>
67
+ {worker.status}
68
+ </span>
69
+ </div>
70
+ {worker.status === 'Present' && (
71
+ <div className="flex justify-between items-center text-[10px] text-gray-500 mt-1">
72
+ <div className="flex items-center gap-1">
73
+ <Clock className="w-3 h-3" />
74
+ <span>
75
+ {worker.punch_in_time ? worker.punch_in_time.substring(0,5) : '--:--'} - {worker.punch_out_time ? worker.punch_out_time.substring(0,5) : '--:--'}
76
+ </span>
77
+ </div>
78
+ {worker.break_hours > 0 ? (
79
+ <span className="bg-gray-100 px-1.5 py-0.5 rounded font-medium">Break: {worker.break_hours}h</span>
80
+ ) : null}
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ )}
87
+ </div>
88
+ ))}
89
+ </div>
90
+ )}
91
+
37
92
  {shifts.length === 0 ? (
38
- <div className="border border-dashed border-gray-200 rounded-xl p-4 flex items-center gap-3 text-gray-400">
39
- <CalendarIcon className="w-5 h-5" />
40
- <div className="text-sm">
41
- <div className="font-semibold text-gray-500">Available for Swap</div>
42
- <div className="text-xs mt-0.5">No shifts scheduled today</div>
93
+ projectAttendances.length === 0 && (
94
+ <div className="border border-dashed border-gray-200 rounded-xl p-4 flex items-center gap-3 text-gray-400">
95
+ <CalendarIcon className="w-5 h-5" />
96
+ <div className="text-sm">
97
+ <div className="font-semibold text-gray-500">Available for Swap</div>
98
+ <div className="text-xs mt-0.5">No shifts scheduled today</div>
99
+ </div>
43
100
  </div>
44
- </div>
101
+ )
45
102
  ) : (
46
- shifts.map((shift, shiftIdx) => (
47
- <div key={shiftIdx} className="space-y-4">
48
- {shift.segmentsInfo.map((info: any, idx: number) => (
49
- <div key={idx} className="border border-gray-200 rounded-xl overflow-hidden relative shadow-sm">
103
+ <div className="space-y-3">
104
+ {projectAttendances.length > 0 && (
105
+ <h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider mt-2">Shifts</h3>
106
+ )}
107
+ {shifts.map((shift, shiftIdx) => (
108
+ <div key={shiftIdx} className="space-y-4">
109
+ {shift.segmentsInfo.map((info: any, idx: number) => (
110
+ <div key={idx} className="border border-gray-200 rounded-xl overflow-hidden relative shadow-sm">
50
111
  <div className="absolute left-0 top-0 bottom-0 w-1 bg-[#0D6EFD]"></div>
51
112
  <div className="p-4 pl-5">
52
113
  <div className="flex justify-between items-start mb-1">
@@ -131,7 +192,8 @@ export default function DailySchedulePanel({ day, events, onCancelClick, onExcha
131
192
  </div>
132
193
  ))}
133
194
  </div>
134
- ))
195
+ ))}
196
+ </div>
135
197
  )}
136
198
  </div>
137
199
  </div>
@@ -32,7 +32,7 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
32
32
 
33
33
  const [currentDate, setCurrentDate] = useState(new Date());
34
34
  const [loading, setLoading] = useState(false);
35
- const [data, setData] = useState<{ holidays: any[], leaves: any[], rosters: any[], exchanges: any[] }>({
35
+ const [data, setData] = useState<{ holidays: any[], leaves: any[], rosters: any[], exchanges: any[], projectAttendance?: Record<string, any> }>({
36
36
  holidays: [], leaves: [], rosters: [], exchanges: []
37
37
  });
38
38
 
@@ -96,14 +96,18 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
96
96
  }
97
97
 
98
98
  if (attendanceRes.status === 'fulfilled' && attendanceRes.value && (attendanceRes.value as any).is_success) {
99
- const days = (attendanceRes.value as any).result?.days || [];
99
+ const days = (attendanceRes.value as any).result?.days || (attendanceRes.value as any).result?.data || [];
100
100
  const map: Record<string, string> = {};
101
+ const fullMap: Record<string, any> = {};
101
102
  days.forEach((d: any) => {
102
- if (d.date && d.status) {
103
- map[d.date] = d.status;
103
+ if (d.date) {
104
+ if (d.status) map[d.date] = d.status;
105
+ fullMap[d.date] = d;
104
106
  }
105
107
  });
106
108
  setAttendanceMap(map);
109
+ // Also save the full day data so we can display project attendance counts
110
+ setData(prev => ({ ...prev, projectAttendance: fullMap }));
107
111
  }
108
112
  } catch {
109
113
  // Silently fail for summary/attendance
@@ -202,6 +206,20 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
202
206
 
203
207
  // Exchanges are now mapped directly into shift segmentsInfo.
204
208
 
209
+ // 4. Project Workforce Attendance (Summary)
210
+ if (data.projectAttendance) {
211
+ const pData = data.projectAttendance[format(day, 'yyyy-MM-dd')];
212
+ if (pData) {
213
+ events.push({
214
+ type: 'project_attendance',
215
+ name: 'Workforce Attendance',
216
+ present: parseInt(pData.present_count || '0', 10),
217
+ absent: parseInt(pData.absent_count || '0', 10),
218
+ workers: pData.workers || [],
219
+ });
220
+ }
221
+ }
222
+
205
223
  return events;
206
224
  };
207
225
 
@@ -373,15 +391,17 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
373
391
  ? evt.isOptional ? 'bg-warning-50 text-warning-700 border border-warning-100 p-1.5' : 'bg-primary-50 text-primary-700 border border-primary-100 p-1.5'
374
392
  : evt.type === 'leave'
375
393
  ? evt.status === 'approved' ? 'bg-success-50 text-success-700 border border-success-100 p-1.5' : 'bg-gray-100 text-gray-700 border border-gray-200 p-1.5'
376
- : evt.type === 'shift'
377
- ? 'bg-[#0D6EFD] text-white p-1.5 flex flex-col gap-0.5'
378
- : evt.type === 'exchange'
379
- ? evt.isPending
380
- ? 'bg-amber-50 text-amber-700 border border-amber-200 p-1.5 border-dashed'
381
- : 'bg-indigo-50 text-indigo-700 border border-indigo-200 p-1.5'
382
- : evt.type === 'off_day'
383
- ? 'bg-white text-red-600 border border-red-200 p-1.5'
384
- : 'bg-purple-50 text-purple-700 border border-purple-200 p-1.5'
394
+ : evt.type === 'project_attendance'
395
+ ? 'bg-emerald-50 text-emerald-700 border border-emerald-200 p-1.5'
396
+ : evt.type === 'shift'
397
+ ? 'bg-[#0D6EFD] text-white p-1.5 flex flex-col gap-0.5'
398
+ : evt.type === 'exchange'
399
+ ? evt.isPending
400
+ ? 'bg-amber-50 text-amber-700 border border-amber-200 p-1.5 border-dashed'
401
+ : 'bg-indigo-50 text-indigo-700 border border-indigo-200 p-1.5'
402
+ : evt.type === 'off_day'
403
+ ? 'bg-white text-red-600 border border-red-200 p-1.5'
404
+ : 'bg-purple-50 text-purple-700 border border-purple-200 p-1.5'
385
405
  }`}
386
406
  >
387
407
  {evt.type === 'shift' ? (
@@ -397,6 +417,14 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
397
417
  </div>
398
418
  )}
399
419
  </>
420
+ ) : evt.type === 'project_attendance' ? (
421
+ <div className="flex flex-col gap-0.5">
422
+ <div className="truncate font-semibold text-xs leading-tight">{evt.name}</div>
423
+ <div className="flex gap-2 text-[10px] opacity-80 mt-0.5">
424
+ <span className="font-medium text-emerald-800">{evt.present} Present</span>
425
+ <span className="font-medium text-red-800">{evt.absent} Absent</span>
426
+ </div>
427
+ </div>
400
428
  ) : (
401
429
  <div className="truncate font-semibold text-xs leading-tight">{evt.name}</div>
402
430
  )}