Skip to content

Commit b0deeee

Browse files
authored
Merge pull request #5764 from RoboJackets/kristaps/inventory-in-attendance
Allow including inventory data during attendance submission
2 parents 1d4026f + 22e5ba9 commit b0deeee

11 files changed

Lines changed: 504 additions & 38 deletions

app/Http/Controllers/AttendanceController.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Models\Team;
1616
use App\Models\User;
1717
use App\Util\AuthorizeInclude;
18+
use App\Util\DeviceInventory;
1819
use Illuminate\Database\Eloquent\Builder;
1920
use Illuminate\Http\JsonResponse;
2021
use Illuminate\Http\Request;
@@ -73,33 +74,47 @@ public function store(StoreAttendanceRequest $request): JsonResponse
7374
}
7475

7576
if ($gtid !== null) {
76-
$user = User::where('gtid', '=', $gtid)->first();
77+
$attendee = User::where('gtid', '=', $gtid)->first();
7778
$attExistingQ = Attendance::where($request->only(['attendable_type', 'attendable_id']))
7879
->where('gtid', '=', $gtid)->whereDate('created_at', $date);
7980
$identifier = ['key' => 'gtid', 'value' => $gtid];
8081
} else {
81-
$user = null;
82+
$attendee = null;
8283
$attExistingQ = Attendance::where(
8384
$request->only(['attendable_type', 'attendable_id', 'access_card_number'])
8485
)
8586
->whereDate('created_at', $date);
8687
$identifier = ['key' => 'access_card_number', 'value' => $request->input('access_card_number')];
8788
}
8889

90+
// Upsert the device if a reader is provided
91+
$device = null;
92+
if ($request->has('reader')) {
93+
$device = DeviceInventory::upsert($request, $request->input('reader'));
94+
}
95+
96+
$attendanceData = $request->validated();
97+
unset($attendanceData['reader']);
98+
8999
$attExistingCount = $attExistingQ->count();
90100
if ($attExistingCount > 0) {
91101
Log::debug(self::class.': Found attendance on '.$date.' for '.$identifier['value'].' - ignoring.');
92102
$att = $attExistingQ->first();
93103
$code = 200;
94104

95-
if ($user !== null) {
96-
PushToJedi::dispatch($user, self::class, -1, 'duplicate-attendance');
105+
if ($attendee !== null) {
106+
PushToJedi::dispatch($attendee, self::class, -1, 'duplicate-attendance');
97107
}
98108
} else {
99109
Log::debug(self::class.': No attendance yet on '.$date.' for '.$identifier['value'].' - saving.');
110+
111+
if ($device !== null) {
112+
$attendanceData['device_serial_number'] = $device->serial_number;
113+
}
114+
100115
$att = Attendance::create(
101116
array_merge(
102-
$request->validated(),
117+
$attendanceData,
103118
[
104119
'recorded_by' => $request->user()->id,
105120
$identifier['key'] => $identifier['value'],

app/Http/Controllers/DeviceController.php

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77
use App\Http\Requests\StoreDeviceRequest;
88
use App\Http\Resources\Device as DeviceResource;
9-
use App\Models\Device;
10-
use App\Models\User;
9+
use App\Util\DeviceInventory;
1110
use Illuminate\Http\JsonResponse;
1211
use Illuminate\Routing\Controllers\HasMiddleware;
1312
use Illuminate\Routing\Controllers\Middleware;
@@ -27,37 +26,7 @@ public static function middleware(): array
2726
*/
2827
public function inventory(StoreDeviceRequest $request): JsonResponse
2928
{
30-
$user = $request->user();
31-
32-
if (! $user instanceof User) {
33-
return response()->json([
34-
'status' => 'error',
35-
'message' => 'A user token is required.',
36-
], 401);
37-
}
38-
39-
$ipAddress = $request->ip();
40-
41-
if ($ipAddress === null) {
42-
return response()->json([
43-
'status' => 'error',
44-
'message' => 'last_seen_ip_address is required.',
45-
], 422);
46-
}
47-
48-
$validated = $request->validated();
49-
50-
$device = Device::updateOrCreate(
51-
['serial_number' => $validated['serial_number']],
52-
array_merge(
53-
$validated,
54-
[
55-
'last_seen_user_id' => $user->id,
56-
'last_seen_at' => now(),
57-
'last_seen_ip_address' => $ipAddress,
58-
]
59-
)
60-
);
29+
$device = DeviceInventory::upsert($request, $request->validated());
6130

6231
$code = $device->wasRecentlyCreated ? 201 : 200;
6332

app/Http/Requests/StoreAttendanceRequest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,34 @@ public function rules(): array
5353
'created_at' => [
5454
'date',
5555
],
56+
'reader' => [
57+
'sometimes',
58+
'array',
59+
],
60+
'reader.serial_number' => [
61+
'required_with:reader',
62+
'digits:7',
63+
],
64+
'reader.hardware_version' => [
65+
'required_with:reader',
66+
'string',
67+
'max:255',
68+
],
69+
'reader.software_version' => [
70+
'required_with:reader',
71+
'string',
72+
'max:255',
73+
],
74+
'reader.firmware_version' => [
75+
'required_with:reader',
76+
'string',
77+
'max:255',
78+
],
79+
'reader.battery_percentage' => [
80+
'required_with:reader',
81+
'integer',
82+
'between:0,100',
83+
],
5684
];
5785
}
5886
}

app/Http/Resources/Attendance.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function toArray(Request $request): array
3232
$this->gtid
3333
),
3434
'access_card_number' => $this->access_card_number,
35+
'device_serial_number' => $this->device_serial_number,
3536
'source' => $this->source,
3637
'recorded_by' => new Manager($this->whenLoaded('recorded')),
3738
'created_at' => $this->created_at,
@@ -40,6 +41,7 @@ public function toArray(Request $request): array
4041

4142
// Relationships
4243
'attendee' => new UserResource($this->whenLoaded('attendee')),
44+
'device' => new Device($this->whenLoaded('device')),
4345
// This deliberately doesn't include the remote attendance link as there is no HTTP resource for it
4446
];
4547
}

app/Models/Attendance.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* @property \Illuminate\Support\Carbon|null $deleted_at
3131
* @property int|null $remote_attendance_link_id
3232
* @property int|null $people_counter_id
33+
* @property int|null $device_serial_number
34+
* @property-read \App\Models\Device|null $device
3335
* @property-read \App\Models\Event|\App\Models\Team $attendable
3436
* @property-read \App\Models\User|null $attendee
3537
* @property-read \App\Models\User|null $recorded
@@ -89,6 +91,7 @@ class Attendance extends Model
8991
public const array RELATIONSHIP_PERMISSIONS = [
9092
'attendee' => 'read-users',
9193
'recorded' => 'read-users',
94+
'device' => 'create-attendance',
9295
];
9396

9497
/**
@@ -141,6 +144,16 @@ public function remoteAttendanceLink(): BelongsTo
141144
return $this->belongsTo(RemoteAttendanceLink::class);
142145
}
143146

147+
/**
148+
* Get the Device that recorded this Attendance model.
149+
*
150+
* @return BelongsTo<Device, Attendance>
151+
*/
152+
public function device(): BelongsTo
153+
{
154+
return $this->belongsTo(Device::class, 'device_serial_number', 'serial_number');
155+
}
156+
144157
/**
145158
* Scope query to start at given date.
146159
*

app/Models/Device.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Eloquent\Factories\HasFactory;
88
use Illuminate\Database\Eloquent\Model;
99
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10+
use Illuminate\Database\Eloquent\Relations\HasMany;
1011

1112
class Device extends Model
1213
{
@@ -69,4 +70,14 @@ public function lastSeenUser(): BelongsTo
6970
{
7071
return $this->belongsTo(User::class, 'last_seen_user_id');
7172
}
73+
74+
/**
75+
* Get the attendance records created with this device.
76+
*
77+
* @return HasMany<\App\Models\Attendance, \App\Models\Device>
78+
*/
79+
public function attendances(): HasMany
80+
{
81+
return $this->hasMany(Attendance::class, 'device_serial_number', 'serial_number');
82+
}
7283
}

app/Nova/Attendance.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public static function uriKey(): string
9191
'recorded',
9292
'attendee',
9393
'attendable',
94+
'device',
9495
];
9596

9697
/**
@@ -131,6 +132,10 @@ public function fields(Request $request): array
131132
->help('The user that recorded the swipe')
132133
->searchable(),
133134

135+
BelongsTo::make('Device', 'device', Device::class)
136+
->searchable()
137+
->canSee(static fn (Request $request): bool => $request->user()->can('create-attendance')),
138+
134139
DateTime::make('Time', 'created_at')
135140
->sortable(),
136141

app/Nova/Device.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace App\Nova;
66

7+
use Illuminate\Http\Request;
78
use Laravel\Nova\Fields\BelongsTo;
89
use Laravel\Nova\Fields\DateTime;
10+
use Laravel\Nova\Fields\HasMany;
911
use Laravel\Nova\Fields\ID;
1012
use Laravel\Nova\Fields\Number;
1113
use Laravel\Nova\Fields\Text;
@@ -121,6 +123,9 @@ public function fields(NovaRequest $request): array
121123

122124
Text::make('Last Seen IP Address', 'last_seen_ip_address'),
123125

126+
HasMany::make('Attendance', 'attendances', Attendance::class)
127+
->canSee(static fn (Request $request): bool => $request->user()->can('read-attendance')),
128+
124129
self::metadataPanel(),
125130
];
126131
}

app/Util/DeviceInventory.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Util;
6+
7+
use App\Models\Device;
8+
use App\Models\User;
9+
use Illuminate\Http\Exceptions\HttpResponseException;
10+
use Illuminate\Http\Request;
11+
12+
class DeviceInventory
13+
{
14+
/**
15+
* Upsert a Device from an inventory report.
16+
*
17+
* Validates the requester is a User and the request has an IP address,
18+
* then updates or creates the device with the provided data and request metadata.
19+
*
20+
* @param array<string, string|int> $data
21+
*
22+
* @throws HttpResponseException
23+
*/
24+
public static function upsert(Request $request, array $data): Device
25+
{
26+
$user = $request->user();
27+
28+
if (! $user instanceof User) {
29+
throw new HttpResponseException(response()->json([
30+
'status' => 'error',
31+
'message' => 'A user token is required.',
32+
], 401));
33+
}
34+
35+
$ipAddress = $request->ip();
36+
37+
if ($ipAddress === null) {
38+
throw new HttpResponseException(response()->json([
39+
'status' => 'error',
40+
'message' => 'last_seen_ip_address is required.',
41+
], 422));
42+
}
43+
44+
return Device::updateOrCreate(
45+
['serial_number' => $data['serial_number']],
46+
array_merge(
47+
$data,
48+
[
49+
'last_seen_user_id' => $user->id,
50+
'last_seen_at' => now(),
51+
'last_seen_ip_address' => $ipAddress,
52+
]
53+
)
54+
);
55+
}
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
return new class extends Migration
10+
{
11+
/**
12+
* Run the migrations.
13+
*/
14+
public function up(): void
15+
{
16+
Schema::table('attendance', static function (Blueprint $table): void {
17+
$table->unsignedInteger('device_serial_number')->nullable();
18+
19+
$table->foreign('device_serial_number', 'attendance_device_serial_number_foreign')
20+
->references('serial_number')
21+
->on('devices');
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*/
28+
public function down(): void
29+
{
30+
Schema::table('attendance', static function (Blueprint $table): void {
31+
$table->dropForeign('attendance_device_serial_number_foreign');
32+
33+
$table->dropColumn('device_serial_number');
34+
});
35+
}
36+
};

0 commit comments

Comments
 (0)