@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,43 @@
1
+ import React from 'react';
2
+ import { Table, THeader, TBody, TRow, TCell } from '../../base-components/Table';
3
+ import { EmptyState } from '../../base-components/EmptyState';
4
+
5
+ export interface AttendanceTimesheetsProps {
6
+ projectId?: string;
7
+ }
8
+
9
+ export default function AttendanceTimesheets({ projectId }: AttendanceTimesheetsProps) {
10
+ // This is a placeholder for the actual HR Attendance Timesheets component.
11
+ // It is designed to be shared across hr-ui and construction-ui.
12
+
13
+ return (
14
+ <div className="flex flex-col w-full h-full">
15
+ <div className="mb-4">
16
+ <h2 className="text-lg font-bold text-gray-900">Timesheets {projectId ? `for Project: ${projectId}` : ''}</h2>
17
+ <p className="text-sm text-gray-500">Manage and view employee attendance records.</p>
18
+ </div>
19
+
20
+ <div className="border border-gray-100 rounded-lg overflow-hidden flex-1">
21
+ <Table>
22
+ <THeader>
23
+ <TRow>
24
+ <TCell isHeader>Employee Name</TCell>
25
+ <TCell isHeader>Date</TCell>
26
+ <TCell isHeader>Shift</TCell>
27
+ <TCell isHeader>Punch In</TCell>
28
+ <TCell isHeader>Punch Out</TCell>
29
+ <TCell isHeader>Status</TCell>
30
+ </TRow>
31
+ </THeader>
32
+ <TBody>
33
+ <TRow>
34
+ <TCell colSpan={6}>
35
+ <EmptyState message="No timesheet records found for this period." />
36
+ </TCell>
37
+ </TRow>
38
+ </TBody>
39
+ </Table>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ const legendItems = [
6
+ { label: "Present", dotClass: "bg-emerald-500" },
7
+ { label: "Absent", dotClass: "bg-red-500" },
8
+ { label: "Leave Approved", dotClass: "bg-emerald-400" },
9
+ { label: "Leave Pending", dotClass: "bg-amber-400" },
10
+ { label: "Holiday", dotClass: "bg-indigo-500" },
11
+ { label: "Weekly Off", dotClass: "bg-gray-400" },
12
+ { label: "OT", dotClass: "bg-purple-500" },
13
+ { label: "On Duty", dotClass: "bg-teal-500" },
14
+ ];
15
+
16
+ export default function CalendarLegend() {
17
+ return (
18
+ <div className="flex items-center gap-4 flex-wrap px-1 py-2">
19
+ {legendItems.map((item) => (
20
+ <div key={item.label} className="flex items-center gap-1.5">
21
+ <span className={`w-2.5 h-2.5 rounded-full ${item.dotClass} ring-2 ring-white shadow-sm`} />
22
+ <span className="text-[11px] font-medium text-gray-500">{item.label}</span>
23
+ </div>
24
+ ))}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,148 @@
1
+ import React, { useState } from 'react';
2
+ import { Modal, Button } from '../../index';
3
+ import { Clock, MapPin, AlertTriangle, AlertCircle } from 'lucide-react';
4
+ import { format } from 'date-fns';
5
+
6
+ interface CancelShiftModalProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ day: Date | null;
10
+ shift: any;
11
+ segmentNo: number | null;
12
+ onConfirm: (reason: string) => void;
13
+ }
14
+
15
+ export default function CancelShiftModal({ isOpen, onClose, day, shift, segmentNo, onConfirm }: CancelShiftModalProps) {
16
+ const [reason, setReason] = useState("");
17
+ const [details, setDetails] = useState("");
18
+ const [acknowledged, setAcknowledged] = useState(false);
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
+ onConfirm(reason);
26
+ setReason("");
27
+ setDetails("");
28
+ setAcknowledged(false);
29
+ };
30
+
31
+ return (
32
+ <Modal isOpen={isOpen} onClose={onClose} size="xl">
33
+ <div className="flex bg-gray-50 h-[500px] overflow-hidden rounded-[16px]">
34
+ {/* Left Panel - Shift Details */}
35
+ <div className="w-[350px] bg-white border-r border-gray-200 p-6 flex flex-col h-full relative">
36
+ <div className="absolute left-0 top-0 bottom-0 w-1.5 bg-[#DC3545]"></div>
37
+
38
+ <div className="text-[#DC3545] text-xs font-bold uppercase tracking-wider mb-2">Cancel Confirmation</div>
39
+ <h2 className="text-3xl font-bold text-gray-900 mb-8">Shift Details</h2>
40
+
41
+ <div className="space-y-6 flex-1">
42
+ <div className="flex gap-4">
43
+ <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0 text-[#0D6EFD]">
44
+ <Clock className="w-5 h-5" />
45
+ </div>
46
+ <div>
47
+ <div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Time & Date</div>
48
+ <div className="text-lg font-bold text-gray-900">{segment?.str}</div>
49
+ <div className="text-sm text-gray-500">{format(day, 'EEEE, MMM d, yyyy')}</div>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="flex gap-4">
54
+ <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0 text-[#0D6EFD]">
55
+ <MapPin className="w-5 h-5" />
56
+ </div>
57
+ <div>
58
+ <div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Location & Role</div>
59
+ <div className="text-lg font-bold text-gray-900">Main Logistics Hub</div>
60
+ <div className="text-sm text-gray-500">{shift.template?.name || 'Shift'}</div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div className="bg-red-50/50 border border-red-100 rounded-xl p-4 mt-auto">
66
+ <div className="flex items-center gap-2 text-red-600 font-bold text-xs uppercase tracking-wider mb-2">
67
+ <AlertTriangle className="w-4 h-4" />
68
+ Cancellation Policy
69
+ </div>
70
+ <p className="text-xs text-gray-600 leading-relaxed">
71
+ Cancellations within 24 hours of start time may result in a performance deduction. Repeated late cancellations can affect your priority status.
72
+ </p>
73
+ </div>
74
+ </div>
75
+
76
+ {/* Right Panel - Reason Form */}
77
+ <div className="flex-1 p-8 flex flex-col bg-white">
78
+ <h2 className="text-2xl font-bold text-gray-900 mb-6">Cancellation Reason</h2>
79
+
80
+ <div className="flex-1 space-y-6">
81
+ <div>
82
+ <label className="block text-sm font-bold text-gray-700 mb-2">
83
+ Primary Reason <span className="text-red-500">*</span>
84
+ </label>
85
+ <select
86
+ className="w-full h-11 border-gray-200 rounded-lg text-gray-700 focus:ring-blue-500 focus:border-blue-500"
87
+ value={reason}
88
+ onChange={(e) => setReason(e.target.value)}
89
+ >
90
+ <option value="">Select a reason...</option>
91
+ <option value="Personal Emergency">Personal Emergency</option>
92
+ <option value="Sick Leave">Sick Leave</option>
93
+ <option value="Transportation Issue">Transportation Issue</option>
94
+ <option value="Family Responsibility">Family Responsibility</option>
95
+ <option value="Other">Other</option>
96
+ </select>
97
+ </div>
98
+
99
+ <div>
100
+ <label className="block text-sm font-bold text-gray-700 mb-2">
101
+ Additional Details (Optional)
102
+ </label>
103
+ <textarea
104
+ className="w-full border-gray-200 rounded-lg text-gray-700 focus:ring-blue-500 focus:border-blue-500 resize-none h-28 p-3"
105
+ placeholder="Briefly explain the situation to your manager..."
106
+ value={details}
107
+ onChange={(e) => setDetails(e.target.value)}
108
+ />
109
+ </div>
110
+
111
+ <label className="flex items-start gap-3 p-4 bg-gray-50 rounded-xl border border-gray-100 cursor-pointer">
112
+ <div className="pt-0.5">
113
+ <input
114
+ type="checkbox"
115
+ className="w-4 h-4 rounded border-gray-300 text-[#0D6EFD] focus:ring-[#0D6EFD]"
116
+ checked={acknowledged}
117
+ onChange={(e) => setAcknowledged(e.target.checked)}
118
+ />
119
+ </div>
120
+ <span className="text-sm text-gray-600">
121
+ I acknowledge that I have read the cancellation policy and understand the potential impact on my schedule reliability score.
122
+ </span>
123
+ </label>
124
+ </div>
125
+
126
+ <div className="flex gap-3 pt-6 mt-auto border-t border-gray-100">
127
+ <Button
128
+ className="flex-1 h-12 bg-red-400 hover:bg-red-500 text-white font-bold text-base"
129
+ isDisabled={!reason || !acknowledged}
130
+ onClick={handleConfirm}
131
+ icon={<AlertCircle className="w-5 h-5" />}
132
+ >
133
+ Confirm Cancellation
134
+ </Button>
135
+ <Button
136
+ variant="flat"
137
+ color="secondary"
138
+ className="flex-1 h-12 bg-[#E2E8F0] hover:bg-gray-300 text-gray-700 font-bold text-base border-0"
139
+ onClick={onClose}
140
+ >
141
+ Keep Shift
142
+ </Button>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </Modal>
147
+ );
148
+ }
@@ -0,0 +1,198 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useCallback } from "react";
4
+ import { format, addDays, subDays } from "date-fns";
5
+ import { getDailyOverview } from "@apptimate/core-lib";
6
+ import { ChevronLeft, ChevronRight, Briefcase, CalendarOff, Palmtree, UserX } from "lucide-react";
7
+ import { Button, TLoader } from "../../index";
8
+
9
+ interface DailyOverviewProps {
10
+ initialDate?: Date;
11
+ }
12
+
13
+ export default function DailyOverview({ initialDate = new Date() }: DailyOverviewProps) {
14
+ const [selectedDate, setSelectedDate] = useState<Date>(initialDate);
15
+ const [data, setData] = useState<any>(null);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+
18
+ const fetchOverview = useCallback(async () => {
19
+ setIsLoading(true);
20
+ try {
21
+ const res = await getDailyOverview(format(selectedDate, "yyyy-MM-dd"));
22
+ if (res.is_success && res.result) {
23
+ setData(res.result);
24
+ }
25
+ } catch (e) {
26
+ console.error(e);
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ }, [selectedDate]);
31
+
32
+ useEffect(() => {
33
+ fetchOverview();
34
+ }, [fetchOverview]);
35
+
36
+ const handlePrevDay = () => setSelectedDate((prev) => subDays(prev, 1));
37
+ const handleNextDay = () => setSelectedDate((prev) => addDays(prev, 1));
38
+ const handleToday = () => setSelectedDate(new Date());
39
+
40
+ return (
41
+ <div className="flex flex-col gap-6">
42
+ <div className="flex items-center justify-between bg-white p-4 rounded-[16px] border border-gray-200 shadow-sm">
43
+ <div className="flex items-center gap-4">
44
+ <h2 className="text-xl font-bold text-gray-900 w-48">{format(selectedDate, "EEEE, MMM d, yyyy")}</h2>
45
+ <div className="flex items-center bg-gray-50 rounded-lg border border-gray-200 p-1">
46
+ <button onClick={handlePrevDay} className="p-1.5 hover:bg-white hover:shadow-sm rounded-md transition-all text-gray-600">
47
+ <ChevronLeft size={18} />
48
+ </button>
49
+ <button onClick={handleToday} className="px-3 py-1.5 text-sm font-medium hover:bg-white hover:shadow-sm rounded-md transition-all text-gray-700">
50
+ Today
51
+ </button>
52
+ <button onClick={handleNextDay} className="p-1.5 hover:bg-white hover:shadow-sm rounded-md transition-all text-gray-600">
53
+ <ChevronRight size={18} />
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ {isLoading ? (
60
+ <div className="h-64 flex items-center justify-center bg-white rounded-[16px] border border-gray-200 shadow-sm">
61
+ <TLoader />
62
+ </div>
63
+ ) : data ? (
64
+ <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start">
65
+
66
+ {/* Working */}
67
+ <div className="bg-white rounded-[16px] border border-gray-200 shadow-sm flex flex-col overflow-hidden">
68
+ <div className="p-4 border-b border-gray-100 flex items-center gap-3 bg-blue-50/50">
69
+ <div className="w-8 h-8 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center">
70
+ <Briefcase size={16} />
71
+ </div>
72
+ <div>
73
+ <h3 className="font-bold text-gray-900 leading-tight">Working</h3>
74
+ <p className="text-xs font-medium text-gray-500">{data.working?.length || 0} Employees</p>
75
+ </div>
76
+ </div>
77
+ <div className="p-2 flex-1 overflow-y-auto max-h-[500px]">
78
+ {data.working && data.working.length > 0 ? (
79
+ <div className="space-y-1">
80
+ {data.working.map((emp: any, idx: number) => (
81
+ <div key={idx} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition-colors group">
82
+ <div className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex flex-shrink-0 items-center justify-center font-bold text-xs uppercase overflow-hidden">
83
+ {emp.avatar ? <img src={emp.avatar} alt={emp.name} className="w-full h-full object-cover" /> : emp.name.charAt(0)}
84
+ </div>
85
+ <div className="flex-1 min-w-0">
86
+ <div className="text-sm font-semibold text-gray-900 truncate">{emp.name}</div>
87
+ <div className="text-[11px] text-gray-500 truncate">{emp.shift?.name || 'Assigned Shift'}</div>
88
+ </div>
89
+ </div>
90
+ ))}
91
+ </div>
92
+ ) : (
93
+ <div className="p-6 text-center text-gray-400 text-sm">No employees working.</div>
94
+ )}
95
+ </div>
96
+ </div>
97
+
98
+ {/* On Leave */}
99
+ <div className="bg-white rounded-[16px] border border-gray-200 shadow-sm flex flex-col overflow-hidden">
100
+ <div className="p-4 border-b border-gray-100 flex items-center gap-3 bg-amber-50/50">
101
+ <div className="w-8 h-8 rounded-lg bg-amber-100 text-amber-600 flex items-center justify-center">
102
+ <CalendarOff size={16} />
103
+ </div>
104
+ <div>
105
+ <h3 className="font-bold text-gray-900 leading-tight">On Leave</h3>
106
+ <p className="text-xs font-medium text-gray-500">{data.on_leave?.length || 0} Employees</p>
107
+ </div>
108
+ </div>
109
+ <div className="p-2 flex-1 overflow-y-auto max-h-[500px]">
110
+ {data.on_leave && data.on_leave.length > 0 ? (
111
+ <div className="space-y-1">
112
+ {data.on_leave.map((emp: any, idx: number) => (
113
+ <div key={idx} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition-colors group">
114
+ <div className="w-8 h-8 rounded-full bg-amber-100 text-amber-600 flex flex-shrink-0 items-center justify-center font-bold text-xs uppercase overflow-hidden">
115
+ {emp.avatar ? <img src={emp.avatar} alt={emp.name} className="w-full h-full object-cover" /> : emp.name.charAt(0)}
116
+ </div>
117
+ <div className="flex-1 min-w-0">
118
+ <div className="text-sm font-semibold text-gray-900 truncate">{emp.name}</div>
119
+ <div className="text-[11px] text-amber-600 font-medium truncate">{emp.leave?.leave_type?.name || 'Leave'}</div>
120
+ </div>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ ) : (
125
+ <div className="p-6 text-center text-gray-400 text-sm">No employees on leave.</div>
126
+ )}
127
+ </div>
128
+ </div>
129
+
130
+ {/* On Holiday */}
131
+ <div className="bg-white rounded-[16px] border border-gray-200 shadow-sm flex flex-col overflow-hidden">
132
+ <div className="p-4 border-b border-gray-100 flex items-center gap-3 bg-purple-50/50">
133
+ <div className="w-8 h-8 rounded-lg bg-purple-100 text-purple-600 flex items-center justify-center">
134
+ <Palmtree size={16} />
135
+ </div>
136
+ <div>
137
+ <h3 className="font-bold text-gray-900 leading-tight">On Holiday</h3>
138
+ <p className="text-xs font-medium text-gray-500">{data.on_holiday?.length || 0} Employees</p>
139
+ </div>
140
+ </div>
141
+ <div className="p-2 flex-1 overflow-y-auto max-h-[500px]">
142
+ {data.on_holiday && data.on_holiday.length > 0 ? (
143
+ <div className="space-y-1">
144
+ {data.on_holiday.map((emp: any, idx: number) => (
145
+ <div key={idx} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition-colors group">
146
+ <div className="w-8 h-8 rounded-full bg-purple-100 text-purple-600 flex flex-shrink-0 items-center justify-center font-bold text-xs uppercase overflow-hidden">
147
+ {emp.avatar ? <img src={emp.avatar} alt={emp.name} className="w-full h-full object-cover" /> : emp.name.charAt(0)}
148
+ </div>
149
+ <div className="flex-1 min-w-0">
150
+ <div className="text-sm font-semibold text-gray-900 truncate">{emp.name}</div>
151
+ <div className="text-[11px] text-purple-600 font-medium truncate">{emp.holiday?.name || 'Holiday'}</div>
152
+ </div>
153
+ </div>
154
+ ))}
155
+ </div>
156
+ ) : (
157
+ <div className="p-6 text-center text-gray-400 text-sm">No holidays today.</div>
158
+ )}
159
+ </div>
160
+ </div>
161
+
162
+ {/* Off Day */}
163
+ <div className="bg-white rounded-[16px] border border-gray-200 shadow-sm flex flex-col overflow-hidden">
164
+ <div className="p-4 border-b border-gray-100 flex items-center gap-3 bg-gray-50">
165
+ <div className="w-8 h-8 rounded-lg bg-gray-200 text-gray-600 flex items-center justify-center">
166
+ <UserX size={16} />
167
+ </div>
168
+ <div>
169
+ <h3 className="font-bold text-gray-900 leading-tight">Off Day</h3>
170
+ <p className="text-xs font-medium text-gray-500">{data.off_days?.length || 0} Employees</p>
171
+ </div>
172
+ </div>
173
+ <div className="p-2 flex-1 overflow-y-auto max-h-[500px]">
174
+ {data.off_days && data.off_days.length > 0 ? (
175
+ <div className="space-y-1">
176
+ {data.off_days.map((emp: any, idx: number) => (
177
+ <div key={idx} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition-colors group">
178
+ <div className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex flex-shrink-0 items-center justify-center font-bold text-xs uppercase overflow-hidden">
179
+ {emp.avatar ? <img src={emp.avatar} alt={emp.name} className="w-full h-full object-cover" /> : emp.name.charAt(0)}
180
+ </div>
181
+ <div className="flex-1 min-w-0">
182
+ <div className="text-sm font-semibold text-gray-900 truncate">{emp.name}</div>
183
+ <div className="text-[11px] text-gray-500 truncate">Rest Day</div>
184
+ </div>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ ) : (
189
+ <div className="p-6 text-center text-gray-400 text-sm">No off days today.</div>
190
+ )}
191
+ </div>
192
+ </div>
193
+
194
+ </div>
195
+ ) : null}
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { Button, PopConfirm } from '../../index';
3
+ import { format } from 'date-fns';
4
+ import { Clock, Calendar as CalendarIcon, X, User, MessageSquare } from 'lucide-react';
5
+
6
+ interface DailySchedulePanelProps {
7
+ day: Date;
8
+ events: any[];
9
+ onCancelClick: (shift: any, segment_no: number) => void;
10
+ onExchangeClick: (shift: any, segment_no: number) => void;
11
+ onCancelExchangeRequest?: (exchangeId: number) => void;
12
+ onClose: () => void;
13
+ }
14
+
15
+ export default function DailySchedulePanel({ day, events, onCancelClick, onExchangeClick, onCancelExchangeRequest, onClose }: DailySchedulePanelProps) {
16
+ const shifts = events.filter(e => e.type === 'shift' && e.segmentsInfo);
17
+
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)]">
20
+ {/* Header */}
21
+ <div className="p-4 border-b border-gray-100 flex items-center justify-between">
22
+ <div className="flex items-center gap-3">
23
+ <h2 className="text-lg font-bold text-gray-900">Daily Schedule</h2>
24
+ <div className="bg-gray-100 text-gray-700 text-xs font-semibold px-2 py-1 rounded-md">
25
+ {format(day, 'EEEE, MMM d')}
26
+ </div>
27
+ </div>
28
+ <button
29
+ onClick={onClose}
30
+ className="p-1 hover:bg-gray-100 rounded-md text-gray-500 transition-colors"
31
+ >
32
+ <X className="w-5 h-5" />
33
+ </button>
34
+ </div>
35
+
36
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
37
+ {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>
43
+ </div>
44
+ </div>
45
+ ) : (
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">
50
+ <div className="absolute left-0 top-0 bottom-0 w-1 bg-[#0D6EFD]"></div>
51
+ <div className="p-4 pl-5">
52
+ <div className="flex justify-between items-start mb-1">
53
+ <h3 className="text-lg font-bold text-gray-900 leading-tight">
54
+ {shift.template?.name || 'Shift'}
55
+ </h3>
56
+ <Clock className="w-4 h-4 text-gray-400" />
57
+ </div>
58
+
59
+ <div className="text-sm text-gray-500 mb-2">Main Logistics Hub</div>
60
+
61
+ <div className="flex items-center gap-2 mb-4">
62
+ <span className={`font-bold text-[#0D6EFD] ${info.isCancelled ? 'line-through opacity-50' : ''}`}>
63
+ {info.str}
64
+ </span>
65
+ <span className="text-[10px] font-semibold text-[#0D6EFD] bg-blue-50 border border-blue-100 px-1.5 py-0.5 rounded-full">
66
+ 8h Total
67
+ </span>
68
+ </div>
69
+
70
+ {info.isCancelled && (
71
+ <div className="mb-4 bg-red-50 text-red-600 text-xs font-bold px-2 py-1 rounded border border-red-100 inline-block">
72
+ CANCELLED
73
+ </div>
74
+ )}
75
+
76
+ {info.isExchangePending && info.pendingExchange ? (
77
+ <div className="mb-4 bg-amber-50 rounded-xl border border-amber-100 p-3">
78
+ <div className="flex items-center justify-between mb-2">
79
+ <span className="text-[10px] font-bold text-amber-600 uppercase tracking-wider bg-amber-100/50 px-2 py-0.5 rounded">
80
+ Swap Pending
81
+ </span>
82
+ {onCancelExchangeRequest && (
83
+ <PopConfirm
84
+ title="Cancel Request?"
85
+ body="Are you sure you want to withdraw this exchange request?"
86
+ onConfirm={(close) => { onCancelExchangeRequest(info.pendingExchange.id); close(); }}
87
+ onCancel={(close) => close()}
88
+ trigger={
89
+ <button className="text-xs font-semibold text-gray-500 hover:text-red-600">
90
+ Abort
91
+ </button>
92
+ }
93
+ />
94
+ )}
95
+ </div>
96
+ <div className="space-y-1.5">
97
+ <div className="flex items-center gap-2 text-xs text-gray-700">
98
+ <User className="w-3.5 h-3.5 text-gray-400" />
99
+ Requested with <span className="font-semibold">{info.pendingExchange.responder?.first_name || 'a colleague'}</span>
100
+ </div>
101
+ {info.pendingExchange.reason && (
102
+ <div className="flex items-start gap-2 text-xs text-gray-600">
103
+ <MessageSquare className="w-3.5 h-3.5 text-gray-400 mt-0.5 flex-shrink-0" />
104
+ <span className="italic line-clamp-2">"{info.pendingExchange.reason}"</span>
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+ ) : (
110
+ <div className="flex items-center gap-2">
111
+ <Button
112
+ size="small"
113
+ variant="bordered"
114
+ className="flex-1 bg-gray-50 hover:bg-gray-100 text-gray-700 border-gray-200 text-xs py-1.5 h-auto font-semibold"
115
+ isDisabled={info.isCancelled}
116
+ onClick={() => onExchangeClick(shift, info.segment_no)}
117
+ >
118
+ Request Exchange
119
+ </Button>
120
+ <Button
121
+ size="small"
122
+ variant="bordered"
123
+ className="flex-1 text-red-600 border-red-200 hover:bg-red-50 text-xs py-1.5 h-auto font-semibold"
124
+ onClick={() => onCancelClick(shift, info.segment_no)}
125
+ >
126
+ {info.isCancelled ? "Restore Shift" : "Cancel Shift"}
127
+ </Button>
128
+ </div>
129
+ )}
130
+ </div>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ ))
135
+ )}
136
+ </div>
137
+ </div>
138
+ );
139
+ }