Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Shift
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 29
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 fromRow
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
132
 getShiftId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmployeeId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getShiftStart
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getShiftEnd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPositionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCreatedByUserId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCreatedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUpdatedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeletedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmployeeName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPositionName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPositionColor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setShiftId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setEmployeeId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setShiftStart
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setShiftEnd
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPositionId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setNotes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setEmployeeName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPositionName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPositionColor
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDurationHours
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isDeleted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 overlaps
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 toDbArray
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 jsonSerialize
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace BuyerKiosk\Scheduling\Models;
4
5use DateTime;
6use DateTimeInterface;
7use DateTimeZone;
8use JsonSerializable;
9
10/**
11 * Shift Entity Model
12 *
13 * Represents a scheduled shift from the scheduleShifts table.
14 * All times are stored in UTC.
15 *
16 * @package BuyerKiosk\Scheduling\Models
17 */
18class Shift implements JsonSerializable
19{
20    private ?int $shiftId = null;
21    private int $employeeId;
22    private DateTime $shiftStart;
23    private DateTime $shiftEnd;
24    private ?int $positionId = null;
25    private ?string $notes = null;
26    private int $createdByUserId;
27    private ?DateTime $createdAt = null;
28    private ?DateTime $updatedAt = null;
29    private ?DateTime $deletedAt = null;
30
31    // Hydrated from JOINs (not persisted)
32    private ?string $employeeName = null;
33    private ?string $positionName = null;
34    private ?string $positionColor = null;
35
36    /**
37     * Create a new Shift instance
38     *
39     * @param int $employeeId Employee ID
40     * @param DateTime $shiftStart Shift start time (UTC)
41     * @param DateTime $shiftEnd Shift end time (UTC)
42     * @param int $createdByUserId User ID who created the shift
43     */
44    public function __construct(
45        int $employeeId,
46        DateTime $shiftStart,
47        DateTime $shiftEnd,
48        int $createdByUserId
49    ) {
50        $this->employeeId = $employeeId;
51        $this->shiftStart = $shiftStart;
52        $this->shiftEnd = $shiftEnd;
53        $this->createdByUserId = $createdByUserId;
54    }
55
56    /**
57     * Create Shift from database row
58     *
59     * @param array $row Database row
60     * @return self
61     */
62    public static function fromRow(array $row): self
63    {
64        $utc = new DateTimeZone('UTC');
65
66        $shift = new self(
67            (int)$row['employeeId'],
68            new DateTime($row['shiftStart'], $utc),
69            new DateTime($row['shiftEnd'], $utc),
70            (int)$row['createdByUserId']
71        );
72
73        $shift->shiftId = isset($row['shiftId']) ? (int)$row['shiftId'] : null;
74        $shift->positionId = isset($row['positionId']) ? (int)$row['positionId'] : null;
75        $shift->notes = $row['notes'] ?? null;
76
77        if (isset($row['created_at'])) {
78            $shift->createdAt = new DateTime($row['created_at'], $utc);
79        }
80        if (isset($row['updated_at'])) {
81            $shift->updatedAt = new DateTime($row['updated_at'], $utc);
82        }
83        if (isset($row['deleted_at']) && $row['deleted_at'] !== null) {
84            $shift->deletedAt = new DateTime($row['deleted_at'], $utc);
85        }
86
87        // Hydrate joined fields if present
88        if (isset($row['employeeName'])) {
89            $shift->employeeName = $row['employeeName'];
90        } elseif (isset($row['employeeFirstName'], $row['employeeLastName'])) {
91            $shift->employeeName = trim($row['employeeFirstName'] . ' ' . $row['employeeLastName']);
92        }
93        if (isset($row['positionName'])) {
94            $shift->positionName = $row['positionName'];
95        }
96        if (isset($row['positionColor'])) {
97            $shift->positionColor = $row['positionColor'];
98        }
99
100        return $shift;
101    }
102
103    // Getters
104
105    public function getShiftId(): ?int
106    {
107        return $this->shiftId;
108    }
109
110    public function getEmployeeId(): int
111    {
112        return $this->employeeId;
113    }
114
115    public function getShiftStart(): DateTime
116    {
117        return $this->shiftStart;
118    }
119
120    public function getShiftEnd(): DateTime
121    {
122        return $this->shiftEnd;
123    }
124
125    public function getPositionId(): ?int
126    {
127        return $this->positionId;
128    }
129
130    public function getNotes(): ?string
131    {
132        return $this->notes;
133    }
134
135    public function getCreatedByUserId(): int
136    {
137        return $this->createdByUserId;
138    }
139
140    public function getCreatedAt(): ?DateTime
141    {
142        return $this->createdAt;
143    }
144
145    public function getUpdatedAt(): ?DateTime
146    {
147        return $this->updatedAt;
148    }
149
150    public function getDeletedAt(): ?DateTime
151    {
152        return $this->deletedAt;
153    }
154
155    public function getEmployeeName(): ?string
156    {
157        return $this->employeeName;
158    }
159
160    public function getPositionName(): ?string
161    {
162        return $this->positionName;
163    }
164
165    public function getPositionColor(): ?string
166    {
167        return $this->positionColor;
168    }
169
170    // Setters
171
172    public function setShiftId(int $shiftId): self
173    {
174        $this->shiftId = $shiftId;
175        return $this;
176    }
177
178    public function setEmployeeId(int $employeeId): self
179    {
180        $this->employeeId = $employeeId;
181        return $this;
182    }
183
184    public function setShiftStart(DateTime $shiftStart): self
185    {
186        $this->shiftStart = $shiftStart;
187        return $this;
188    }
189
190    public function setShiftEnd(DateTime $shiftEnd): self
191    {
192        $this->shiftEnd = $shiftEnd;
193        return $this;
194    }
195
196    public function setPositionId(?int $positionId): self
197    {
198        $this->positionId = $positionId;
199        return $this;
200    }
201
202    public function setNotes(?string $notes): self
203    {
204        $this->notes = $notes;
205        return $this;
206    }
207
208    public function setEmployeeName(?string $name): self
209    {
210        $this->employeeName = $name;
211        return $this;
212    }
213
214    public function setPositionName(?string $name): self
215    {
216        $this->positionName = $name;
217        return $this;
218    }
219
220    public function setPositionColor(?string $color): self
221    {
222        $this->positionColor = $color;
223        return $this;
224    }
225
226    // Business Logic
227
228    /**
229     * Calculate shift duration in hours
230     *
231     * @return float Duration in hours
232     */
233    public function getDurationHours(): float
234    {
235        $diff = $this->shiftEnd->getTimestamp() - $this->shiftStart->getTimestamp();
236        return $diff / 3600;
237    }
238
239    /**
240     * Check if shift is deleted (soft delete)
241     *
242     * @return bool
243     */
244    public function isDeleted(): bool
245    {
246        return $this->deletedAt !== null;
247    }
248
249    /**
250     * Check if this shift overlaps with another time range
251     *
252     * @param DateTime $start Start of range to check
253     * @param DateTime $end End of range to check
254     * @return bool True if shifts overlap
255     */
256    public function overlaps(DateTime $start, DateTime $end): bool
257    {
258        // Overlap occurs if one shift starts before the other ends
259        // and ends after the other starts
260        return $this->shiftStart < $end && $this->shiftEnd > $start;
261    }
262
263    /**
264     * Convert to array for database insert/update
265     *
266     * @return array
267     */
268    public function toDbArray(): array
269    {
270        return [
271            'employeeId' => $this->employeeId,
272            'shiftStart' => $this->shiftStart->format('Y-m-d H:i:s'),
273            'shiftEnd' => $this->shiftEnd->format('Y-m-d H:i:s'),
274            'positionId' => $this->positionId,
275            'notes' => $this->notes,
276            'createdByUserId' => $this->createdByUserId,
277        ];
278    }
279
280    /**
281     * JSON serialization for API responses
282     *
283     * @return array
284     */
285    public function jsonSerialize(): array
286    {
287        return [
288            'shiftId' => $this->shiftId,
289            'employeeId' => $this->employeeId,
290            'employeeName' => $this->employeeName,
291            'shiftStart' => $this->shiftStart->format(DateTimeInterface::ATOM),
292            'shiftEnd' => $this->shiftEnd->format(DateTimeInterface::ATOM),
293            'positionId' => $this->positionId,
294            'position' => $this->positionName,
295            'positionColor' => $this->positionColor,
296            'notes' => $this->notes,
297            'updatedAt' => $this->updatedAt?->format(DateTimeInterface::ATOM),
298        ];
299    }
300}