@apptimate/ui 5.8.0 → 6.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "5.8.0",
3
+ "version": "6.0.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -10,9 +10,12 @@
10
10
  "clsx": "^2.1.1",
11
11
  "date-fns": "^4.4.0",
12
12
  "framer-motion": "^12.38.0",
13
+ "html2pdf.js": "^0.14.0",
13
14
  "lucide-react": "^1.14.0",
15
+ "qrcode.react": "^4.2.0",
14
16
  "react": "^19.2.5",
15
17
  "react-apexcharts": "^2.1.1",
18
+ "react-barcode": "^1.6.1",
16
19
  "recharts": "^2.15.4",
17
20
  "tailwind-merge": "^3.5.0",
18
21
  "use-debounce": "^10.1.1",
@@ -208,7 +208,11 @@ function SelectCore<T extends Record<string, unknown>>({
208
208
  // ── Default value ────────────────────────────────────────────────────────
209
209
  const defaultValString = JSON.stringify(defaultValue || '');
210
210
  useEffect(() => {
211
- if (!defaultValue) return;
211
+ if (!defaultValue) {
212
+ setSelectedValue(multiple ? [] : ({} as T));
213
+ setValue(multiple ? [] : '');
214
+ return;
215
+ }
212
216
  if (multiple && Array.isArray(defaultValue) && defaultValue.length > 0) {
213
217
  const vals = (defaultValue as T[]).map((v) => v[opt.value] as string | number);
214
218
  setSelectedValue(defaultValue as T[]);
@@ -25,7 +25,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
25
25
  <select
26
26
  ref={ref}
27
27
  className={cn(
28
- "w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 appearance-none cursor-pointer",
28
+ "w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 appearance-none cursor-pointer disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed",
29
29
  error && "border-danger-alt focus:border-danger-alt hover:border-danger-alt"
30
30
  )}
31
31
  {...props}
@@ -60,8 +60,8 @@ export const TCell = ({ children, className, isHeader = false, colSpan, rowSpan,
60
60
  !isHeader && "flex justify-between items-center lg:table-cell border-b border-gray-100 last:border-b-0 lg:last:border-b", // Adds separator line across rows on desktop
61
61
  className
62
62
  )}>
63
- {label && <span className="lg:hidden text-[11px] font-bold text-gray-400 uppercase tracking-wider">{label}</span>}
64
- <div className="flex justify-end lg:block w-full lg:w-auto">{children}</div>
63
+ {label && <span className="lg:hidden text-[11px] font-bold text-gray-400 uppercase tracking-wider shrink-0 mr-4">{label}</span>}
64
+ <div className="flex justify-end lg:block flex-1 min-w-0 lg:w-auto">{children}</div>
65
65
  </Tag>
66
66
  );
67
67
  };
@@ -36,6 +36,7 @@ export type OrganizationConfig = {
36
36
  name: string;
37
37
  code?: string;
38
38
  organization_type?: string;
39
+ parent_organization_id?: number;
39
40
  };
40
41
 
41
42
  export function DashboardLayout({
@@ -71,11 +72,40 @@ export function DashboardLayout({
71
72
  const pathname = usePathname() || "";
72
73
  const router = useRouter();
73
74
 
75
+ // Helper to build hierarchy
76
+ const flattenedOrganizations = React.useMemo(() => {
77
+ if (!organizations || organizations.length === 0) return [];
78
+
79
+ const orgMap = new Map<number, any>();
80
+ const roots: any[] = [];
81
+
82
+ organizations.forEach(org => {
83
+ orgMap.set(org.id, { ...org, children: [] });
84
+ });
85
+
86
+ organizations.forEach(org => {
87
+ const node = orgMap.get(org.id)!;
88
+ if (org.parent_organization_id && orgMap.has(org.parent_organization_id)) {
89
+ orgMap.get(org.parent_organization_id)!.children.push(node);
90
+ } else {
91
+ roots.push(node);
92
+ }
93
+ });
94
+
95
+ const flattened: (OrganizationConfig & { _level: number })[] = [];
96
+ const traverse = (nodes: any[], level: number) => {
97
+ nodes.forEach(node => {
98
+ flattened.push({ ...node, _level: level });
99
+ traverse(node.children, level + 1);
100
+ });
101
+ };
102
+ traverse(roots, 0);
103
+ return flattened;
104
+ }, [organizations]);
105
+
74
106
  // Find which main menu should be active based on current path.
75
107
  // When basePath is set, prefer the menu whose items are mostly within basePath.
76
- const fullPath = (basePath && pathname.startsWith(basePath))
77
- ? pathname
78
- : basePath + (pathname === "/" && basePath ? "" : pathname);
108
+ const fullPath = basePath + (pathname === "/" && basePath ? "" : pathname);
79
109
  const currentMainMenu = (() => {
80
110
  let bestMenu = menus[0];
81
111
  let bestScore = -1;
@@ -213,18 +243,15 @@ export function DashboardLayout({
213
243
  setIsSubSidebarOpen(true);
214
244
  }
215
245
  }}
216
- className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-all duration-200 w-full px-1 py-1 relative ${isActive ? "text-indigo-600" : "text-gray-400 hover:text-[#2D3142]"
246
+ className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors w-full px-1 ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
217
247
  }`}
218
248
  >
219
- {isActive && (
220
- <span className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-indigo-600 rounded-r-full" />
221
- )}
222
249
  {/* Clone the icon to dynamically apply styling based on active state */}
223
250
  {React.cloneElement(menu.icon as React.ReactElement<any>, {
224
251
  size: 22,
225
252
  className: isActive ? "stroke-[2.5]" : "stroke-[2]"
226
253
  })}
227
- <span className={`text-[11px] text-center leading-tight truncate block w-full max-w-[70px] ${isActive ? "font-bold text-indigo-700" : "font-medium"}`}>{menu.label}</span>
254
+ <span className={`text-[11px] text-center leading-tight truncate block w-full max-w-[70px] ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
228
255
  </div>
229
256
  );
230
257
  })}
@@ -291,9 +318,9 @@ export function DashboardLayout({
291
318
  : !externalPaths.some(ext => item.path.startsWith(ext));
292
319
  const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
293
320
 
294
- const className = `pl-3 pr-4 py-2 rounded-r-lg rounded-l-none text-sm transition-all duration-200 flex items-center justify-between border-l-4 ${isItemActive
295
- ? "bg-indigo-50/70 border-indigo-600 text-indigo-950 font-bold"
296
- : "border-transparent text-gray-500 font-medium hover:bg-gray-50/50"
321
+ const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
322
+ ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
323
+ : "text-gray-500 font-medium hover:bg-gray-50"
297
324
  }`;
298
325
 
299
326
  const content = (
@@ -329,7 +356,12 @@ export function DashboardLayout({
329
356
  onClick={() => setIsOrgModalOpen(true)}
330
357
  className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
331
358
  >
332
- <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0 shadow-sm">
359
+ <div className={cn(
360
+ "w-8 h-8 rounded-lg flex items-center justify-center shrink-0 shadow-sm",
361
+ selectedOrganization?.organization_type === 'project'
362
+ ? "bg-gradient-to-br from-teal-400 to-teal-600"
363
+ : "bg-gradient-to-br from-indigo-500 to-purple-600"
364
+ )}>
333
365
  <Building2 size={14} className="text-white" />
334
366
  </div>
335
367
  <div className="flex-1 min-w-0 text-left">
@@ -361,7 +393,12 @@ export function DashboardLayout({
361
393
  onClick={() => setIsOrgModalOpen(true)}
362
394
  className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-[#F4F5F7] hover:bg-gray-200/70 transition-all cursor-pointer"
363
395
  >
364
- <div className="w-5 h-5 rounded-md bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0">
396
+ <div className={cn(
397
+ "w-5 h-5 rounded-md flex items-center justify-center shrink-0",
398
+ selectedOrganization?.organization_type === 'project'
399
+ ? "bg-gradient-to-br from-teal-400 to-teal-600"
400
+ : "bg-gradient-to-br from-indigo-500 to-purple-600"
401
+ )}>
365
402
  <Building2 size={10} className="text-white" />
366
403
  </div>
367
404
  <span className="text-[12px] font-semibold text-[#2D3142] max-w-[100px] truncate">
@@ -526,32 +563,37 @@ export function DashboardLayout({
526
563
  {/* Organization List */}
527
564
  <div className="px-4 py-3 max-h-[360px] overflow-y-auto">
528
565
  <div className="flex flex-col gap-1">
529
- {organizations.map((org) => {
566
+ {flattenedOrganizations.map((org) => {
530
567
  const isSelected = selectedOrganization?.id === org.id;
568
+ const pl = org._level > 0 ? org._level * 24 : 0;
569
+ const isProject = org.organization_type === 'project';
570
+
531
571
  return (
532
572
  <button
533
573
  key={org.id}
534
574
  id={`org-option-${org.id}`}
535
575
  onClick={() => handleSelectOrg(org)}
576
+ style={{ paddingLeft: pl ? (pl + 16) + 'px' : undefined }}
536
577
  className={cn(
537
578
  "w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-150 cursor-pointer",
538
579
  isSelected
539
- ? "bg-indigo-50 border border-indigo-200 shadow-sm"
540
- : "hover:bg-gray-50 border border-transparent"
580
+ ? (isProject ? "bg-teal-50 border border-teal-200 shadow-sm" : "bg-indigo-50 border border-indigo-200 shadow-sm")
581
+ : (isProject ? "hover:bg-teal-50 border border-transparent" : "hover:bg-gray-50 border border-transparent"),
582
+ org._level > 0 ? "relative before:content-[''] before:absolute before:left-[-12px] before:top-1/2 before:w-3 before:h-px before:bg-gray-200" : ""
541
583
  )}
542
584
  >
543
585
  <div className={cn(
544
586
  "w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors",
545
587
  isSelected
546
- ? "bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm"
547
- : "bg-gray-100"
588
+ ? (isProject ? "bg-gradient-to-br from-teal-400 to-teal-600 shadow-sm" : "bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm")
589
+ : (isProject ? "bg-teal-100/50" : "bg-gray-100")
548
590
  )}>
549
- <Building2 size={14} className={isSelected ? "text-white" : "text-gray-500"} />
591
+ <Building2 size={14} className={isSelected ? "text-white" : (isProject ? "text-teal-600" : "text-gray-500")} />
550
592
  </div>
551
593
  <div className="flex-1 min-w-0">
552
594
  <p className={cn(
553
595
  "text-[14px] font-semibold truncate leading-tight",
554
- isSelected ? "text-indigo-700" : "text-[#2D3142]"
596
+ isSelected ? (isProject ? "text-teal-700" : "text-indigo-700") : (isProject ? "text-teal-700" : "text-[#2D3142]")
555
597
  )}>
556
598
  {org.name}
557
599
  </p>
@@ -563,7 +605,7 @@ export function DashboardLayout({
563
605
  </p>
564
606
  </div>
565
607
  {isSelected && (
566
- <div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center shrink-0">
608
+ <div className={cn("w-6 h-6 rounded-full flex items-center justify-center shrink-0", isProject ? "bg-teal-500" : "bg-indigo-500")}>
567
609
  <Check size={12} className="text-white stroke-[3]" />
568
610
  </div>
569
611
  )}
@@ -0,0 +1,380 @@
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 { Checkbox } from "../../base-components/Checkbox";
7
+ import { Table, THeader, TRow, TCell, TBody } from "../../base-components/Table";
8
+ import { bulkPunchAttendance, getWorkforceAttendanceLookup, getEmployeeLookup } from "@apptimate/core-lib";
9
+ import { Plus } from "lucide-react";
10
+ import toast from "react-hot-toast";
11
+
12
+ interface BulkAttendanceModalProps {
13
+ isOpen: boolean;
14
+ onClose: () => void;
15
+ onSuccess: () => void;
16
+ fetchProjects?: () => Promise<any>;
17
+ initialProjectId?: string;
18
+ isFixedProject?: boolean;
19
+ }
20
+
21
+ export function BulkAttendanceModal({ isOpen, onClose, onSuccess, fetchProjects, initialProjectId, isFixedProject }: BulkAttendanceModalProps) {
22
+ const [projects, setProjects] = useState<any[]>([]);
23
+ const [projectId, setProjectId] = useState<string>(initialProjectId || "");
24
+ const [attendanceDate, setAttendanceDate] = useState<string>(new Date().toISOString().split("T")[0]);
25
+ const [employees, setEmployees] = useState<any[]>([]);
26
+ const [globalEmployees, setGlobalEmployees] = useState<any[]>([]);
27
+ const [isLoadingEmployees, setIsLoadingEmployees] = useState(false);
28
+ const [isSubmitting, setIsSubmitting] = useState(false);
29
+
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 }>>({});
32
+
33
+ useEffect(() => {
34
+ if (isOpen) {
35
+ loadProjects();
36
+ loadGlobalEmployees();
37
+ setAttendanceDate(new Date().toISOString().split("T")[0]);
38
+ setProjectId(initialProjectId || "");
39
+ setEmployees([]);
40
+ setTimeEntries({});
41
+ }
42
+ }, [isOpen, initialProjectId]);
43
+
44
+ useEffect(() => {
45
+ if (projectId && attendanceDate) {
46
+ loadEmployees();
47
+ } else {
48
+ setEmployees([]);
49
+ }
50
+ }, [projectId, attendanceDate]);
51
+
52
+ const loadGlobalEmployees = async () => {
53
+ try {
54
+ const res = await getEmployeeLookup();
55
+ if (res.is_success && res.result) {
56
+ setGlobalEmployees(res.result);
57
+ }
58
+ } catch (e) {
59
+ console.error(e);
60
+ }
61
+ };
62
+
63
+ const loadProjects = async () => {
64
+ try {
65
+ if (fetchProjects) {
66
+ const res = await fetchProjects();
67
+ if (res.is_success && res.result?.data) {
68
+ setProjects(res.result.data);
69
+ } else if (res.is_success && Array.isArray(res.result)) {
70
+ setProjects(res.result);
71
+ }
72
+ }
73
+ } catch (e) {
74
+ console.error(e);
75
+ }
76
+ };
77
+
78
+ const loadEmployees = async () => {
79
+ try {
80
+ setIsLoadingEmployees(true);
81
+ const res = await getWorkforceAttendanceLookup({ project_id: Number(projectId), date: attendanceDate });
82
+ if (res.is_success && res.result) {
83
+ setEmployees(res.result);
84
+
85
+ // Initialize time entries for these employees
86
+ const newEntries: Record<string, { in: string, out: string, break: number, isPresent: boolean }> = {};
87
+ res.result.forEach((emp: any) => {
88
+ newEntries[emp.id] = { in: "09:00", out: "18:00", break: 0, isPresent: true };
89
+ });
90
+ setTimeEntries(newEntries);
91
+ }
92
+ } catch (e: any) {
93
+ toast.error("Failed to load allocated employees");
94
+ } finally {
95
+ setIsLoadingEmployees(false);
96
+ }
97
+ };
98
+
99
+ const handleTimeChange = (empId: string, type: 'in' | 'out' | 'break' | 'isPresent', value: string | number | boolean) => {
100
+ setTimeEntries(prev => ({
101
+ ...prev,
102
+ [empId]: {
103
+ ...prev[empId],
104
+ [type]: value
105
+ }
106
+ }));
107
+ };
108
+
109
+ const handleAddEmployeeRow = () => {
110
+ const fakeId = `manual-${Date.now()}`;
111
+ setEmployees(prev => [...prev, { id: fakeId, is_manual: true }]);
112
+ setTimeEntries(prev => ({
113
+ ...prev,
114
+ [fakeId]: { in: "09:00", out: "18:00", break: 0, isPresent: true }
115
+ }));
116
+ };
117
+
118
+ const handleEmployeeSelectChange = (fakeId: string, newEmpId: string) => {
119
+ const selectedEmp = globalEmployees.find((e) => String(e.id) === newEmpId);
120
+ if (!selectedEmp) return;
121
+
122
+ setEmployees(prev => prev.map(emp =>
123
+ emp.id === fakeId
124
+ ? { ...emp, id: newEmpId, first_name: selectedEmp.first_name, last_name: selectedEmp.last_name, employee_code: selectedEmp.employee_code, is_manual: true }
125
+ : emp
126
+ ));
127
+
128
+ setTimeEntries(prev => {
129
+ const newEntries = { ...prev };
130
+ newEntries[newEmpId] = newEntries[fakeId];
131
+ delete newEntries[fakeId];
132
+ return newEntries;
133
+ });
134
+ };
135
+
136
+ const handleRemoveEmployeeRow = (empId: string) => {
137
+ setEmployees(prev => prev.filter(emp => emp.id !== empId));
138
+ setTimeEntries(prev => {
139
+ const newEntries = { ...prev };
140
+ delete newEntries[empId];
141
+ return newEntries;
142
+ });
143
+ };
144
+
145
+ const handleSubmit = async () => {
146
+ if (employees.length === 0) {
147
+ toast.error("No employees to log attendance for");
148
+ return;
149
+ }
150
+
151
+ try {
152
+ setIsSubmitting(true);
153
+
154
+ const hrPunches: any[] = [];
155
+ const workforcePunches: any[] = [];
156
+
157
+ employees.forEach(emp => {
158
+ const entry = timeEntries[emp.id];
159
+
160
+ if (!entry || !entry.isPresent) return;
161
+
162
+ if (String(emp.id).startsWith('wf-')) {
163
+ // It's a workforce gang worker
164
+ const parts = String(emp.id).split('-'); // e.g. wf-12-1
165
+ const workforceId = parts[1];
166
+ const memberIdentifier = parts[2] || null;
167
+
168
+ if (entry.in || entry.out) {
169
+ workforcePunches.push({
170
+ workforce_id: Number(workforceId),
171
+ project_id: Number(projectId),
172
+ date: attendanceDate,
173
+ member_identifier: memberIdentifier,
174
+ punch_in_time: entry.in || null,
175
+ punch_out_time: entry.out || null,
176
+ break_hours: entry.break || 0,
177
+ });
178
+ }
179
+ } else {
180
+ // It's a standard HR employee
181
+ if (entry.in) {
182
+ hrPunches.push({
183
+ employee_id: emp.id,
184
+ punch_at: `${attendanceDate} ${entry.in}:00`,
185
+ type: "I",
186
+ source: "Bulk"
187
+ });
188
+ }
189
+ if (entry.out) {
190
+ hrPunches.push({
191
+ employee_id: emp.id,
192
+ punch_at: `${attendanceDate} ${entry.out}:00`,
193
+ type: "O",
194
+ source: "Bulk"
195
+ });
196
+ }
197
+ }
198
+ });
199
+
200
+ if (hrPunches.length === 0 && workforcePunches.length === 0) {
201
+ toast.error("Please fill in at least one time entry");
202
+ setIsSubmitting(false);
203
+ return;
204
+ }
205
+
206
+ let hasError = false;
207
+
208
+ // Log HR punches
209
+ if (hrPunches.length > 0) {
210
+ const hrRes = await bulkPunchAttendance({ punches: hrPunches });
211
+ if (!hrRes.is_success) {
212
+ toast.error(hrRes.message || "Failed to log HR attendance");
213
+ hasError = true;
214
+ }
215
+ }
216
+
217
+ // Log Workforce punches
218
+ if (workforcePunches.length > 0) {
219
+ // use sendRequest from core-lib which is imported at the top of this file
220
+ 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");
224
+ hasError = true;
225
+ }
226
+ }
227
+
228
+ if (!hasError) {
229
+ toast.success("Bulk attendance logged successfully");
230
+ onSuccess();
231
+ onClose();
232
+ } else {
233
+ toast.error("Failed to log some attendance records");
234
+ }
235
+ } catch (e: any) {
236
+ toast.error(e.message || "An error occurred");
237
+ } finally {
238
+ setIsSubmitting(false);
239
+ }
240
+ };
241
+
242
+ return (
243
+ <Modal isOpen={isOpen} onClose={onClose} title="Bulk Attendance Log" size="2xl">
244
+ <div className="space-y-4 py-2">
245
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
246
+ <div>
247
+ <label className="block text-sm font-medium text-gray-700 mb-1">Date <span className="text-danger-500">*</span></label>
248
+ <Input
249
+ type="date"
250
+ value={attendanceDate}
251
+ onChange={(e) => setAttendanceDate(e.target.value)}
252
+ />
253
+ </div>
254
+ <div>
255
+ <label className="block text-sm font-medium text-gray-700 mb-1">Project <span className="text-danger-500">*</span></label>
256
+ <Select
257
+ value={projectId}
258
+ onChange={(e) => setProjectId(e.target.value)}
259
+ 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
+ />
268
+ </div>
269
+ </div>
270
+
271
+ {projectId && attendanceDate && (
272
+ <div className="mt-6 border border-gray-100 rounded-xl overflow-hidden bg-white shadow-sm">
273
+ <Table>
274
+ <THeader>
275
+ <TRow>
276
+ <TCell isHeader className="bg-gray-50 text-gray-600 font-semibold tracking-wide border-t-0 border-r-0 border-l-0">Employee</TCell>
277
+ <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
+ <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
+ <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>
281
+ </TRow>
282
+ </THeader>
283
+ <TBody>
284
+ {isLoadingEmployees ? (
285
+ <TRow>
286
+ <TCell colSpan={4} className="text-center py-8 text-gray-400">Loading employees...</TCell>
287
+ </TRow>
288
+ ) : employees.length === 0 ? (
289
+ <TRow>
290
+ <TCell colSpan={4} className="text-center py-8 text-gray-500">No employees allocated to this project on this date.</TCell>
291
+ </TRow>
292
+ ) : (
293
+ employees.map(emp => (
294
+ <TRow key={emp.id} className="hover:bg-gray-50/50 transition-colors">
295
+ <TCell label="Employee">
296
+ {emp.is_manual ? (
297
+ <div className="flex items-center gap-2">
298
+ <Select
299
+ value={String(emp.id).startsWith('manual') ? "" : String(emp.id)}
300
+ onChange={(e) => handleEmployeeSelectChange(emp.id, e.target.value)}
301
+ options={[
302
+ { value: "", label: "Select Employee..." },
303
+ ...globalEmployees.map(ge => ({
304
+ value: String(ge.id),
305
+ label: `${ge.first_name} ${ge.last_name}${ge.employee_code ? ` (${ge.employee_code})` : ''}`
306
+ }))
307
+ ]}
308
+ />
309
+ <button onClick={() => handleRemoveEmployeeRow(emp.id)} className="text-red-500 p-1 hover:bg-red-50 rounded">
310
+ &times;
311
+ </button>
312
+ </div>
313
+ ) : (
314
+ <>
315
+ <div className="font-semibold text-[#2D3142]">{emp.first_name} {emp.last_name}</div>
316
+ {emp.employee_code && <div className="text-xs text-gray-500">{emp.employee_code}</div>}
317
+ </>
318
+ )}
319
+ </TCell>
320
+ <TCell label="Present">
321
+ <Checkbox
322
+ checked={timeEntries[emp.id]?.isPresent || false}
323
+ onChange={(e) => handleTimeChange(emp.id, 'isPresent', e.target.checked)}
324
+ />
325
+ </TCell>
326
+ <TCell label="First In">
327
+ <Input
328
+ type="time"
329
+ value={timeEntries[emp.id]?.in || ""}
330
+ onChange={(e) => handleTimeChange(emp.id, 'in', e.target.value)}
331
+ className="h-9 w-32"
332
+ disabled={!timeEntries[emp.id]?.isPresent}
333
+ />
334
+ </TCell>
335
+ <TCell label="Last Out">
336
+ <Input
337
+ type="time"
338
+ value={timeEntries[emp.id]?.out || ""}
339
+ onChange={(e) => handleTimeChange(emp.id, 'out', e.target.value)}
340
+ className="h-9 w-32"
341
+ disabled={!timeEntries[emp.id]?.isPresent}
342
+ />
343
+ </TCell>
344
+ <TCell label="Break (hrs)">
345
+ <Input
346
+ type="number"
347
+ step="0.5"
348
+ min="0"
349
+ value={timeEntries[emp.id]?.break || 0}
350
+ onChange={(e) => handleTimeChange(emp.id, 'break', Number(e.target.value))}
351
+ className="h-9 w-32"
352
+ disabled={!timeEntries[emp.id]?.isPresent || !String(emp.id).startsWith('wf-')}
353
+ title={!String(emp.id).startsWith('wf-') ? "Break hours are only applicable to Construction Workforce" : ""}
354
+ />
355
+ </TCell>
356
+ </TRow>
357
+ ))
358
+ )}
359
+ </TBody>
360
+ </Table>
361
+ <div className="p-3 border-t border-gray-100 bg-gray-50">
362
+ <Button variant="flat" color="secondary" onClick={handleAddEmployeeRow} icon={<Plus size={16} />} iconPosition="left">
363
+ Add Row
364
+ </Button>
365
+ </div>
366
+ </div>
367
+ )}
368
+ </div>
369
+
370
+ <ModalFooter>
371
+ <Button variant="flat" color="secondary" onClick={onClose} isDisabled={isSubmitting}>
372
+ Cancel
373
+ </Button>
374
+ <Button color="primary" onClick={handleSubmit} isLoading={isSubmitting} isDisabled={employees.length === 0}>
375
+ Save All
376
+ </Button>
377
+ </ModalFooter>
378
+ </Modal>
379
+ );
380
+ }
@@ -16,7 +16,7 @@ export default function DailySchedulePanel({ day, events, onCancelClick, onExcha
16
16
  const shifts = events.filter(e => e.type === 'shift' && e.segmentsInfo);
17
17
 
18
18
  return (
19
- <div className="w-80 flex-shrink-0 bg-white rounded-[16px] border border-gray-200 overflow-hidden flex flex-col h-[calc(100vh-140px)]">
19
+ <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">
20
20
  {/* Header */}
21
21
  <div className="p-4 border-b border-gray-100 flex items-center justify-between">
22
22
  <div className="flex items-center gap-3">
@@ -89,7 +89,7 @@ export function ManualPunchModal({ isOpen, onClose, onSuccess, fetchEmployees }:
89
89
  onChange={(e) => setFormData({ ...formData, employee_id: e.target.value })}
90
90
  options={employees.map((emp) => ({
91
91
  value: String(emp.id),
92
- label: `${emp.first_name} ${emp.last_name} (${emp.employee_code || ''})`
92
+ label: `${emp.first_name} ${emp.last_name}${emp.employee_code ? ` (${emp.employee_code})` : ''}`
93
93
  }))}
94
94
  />
95
95
  </div>
@@ -287,17 +287,17 @@ export default function ShiftCalendar({ employeeId, projectId, workforceId, show
287
287
  };
288
288
 
289
289
  return (
290
- <div className="flex gap-4 items-start w-full">
290
+ <div className="flex flex-col lg:flex-row gap-4 items-start w-full relative">
291
291
  {/* Main Calendar View */}
292
- <div className="flex-1 bg-white rounded-[16px] border border-gray-200 overflow-hidden shadow-sm flex flex-col relative h-[calc(100vh-140px)]">
292
+ <div className="flex-1 w-full bg-white rounded-[16px] border border-gray-200 overflow-hidden shadow-sm flex flex-col relative h-[calc(100vh-140px)]">
293
293
  {/* Header */}
294
- <div className="p-4 border-b border-gray-100 flex items-center justify-between">
294
+ <div className="p-4 border-b border-gray-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
295
295
  <h2 className="text-xl font-bold text-[#2D3142]">
296
296
  {viewMode === 'month'
297
297
  ? format(currentDate, 'MMMM yyyy')
298
298
  : `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d, yyyy')}`}
299
299
  </h2>
300
- <div className="flex items-center gap-4">
300
+ <div className="flex flex-wrap items-center gap-3 sm:gap-4">
301
301
  <div className="flex p-0.5 bg-gray-100 rounded-lg">
302
302
  <button
303
303
  onClick={() => setViewMode('month')}