diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py index 29f99076..f0aa79df 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py @@ -147,3 +147,65 @@ class FpTabletController(http.Controller): 'error': _('Incorrect PIN.'), 'attempts_remaining': threshold - new_count, } + + # ====================================================================== + # /fp/tablet/tiles — lock-screen tile grid + # ====================================================================== + @http.route('/fp/tablet/tiles', type='jsonrpc', auth='user') + def tiles(self, station_id=None): + env = request.env + op_group = env.ref( + 'fusion_plating.group_fusion_plating_operator', + raise_if_not_found=False, + ) + if not op_group: + return {'ok': False, 'error': 'operator group missing'} + + # Determine candidate users — station roster wins if non-empty + users = op_group.user_ids + if station_id: + Station = env['fusion.plating.shopfloor.station'] + station = Station.browse(int(station_id)) + if (station.exists() + and 'x_fc_authorised_user_ids' in station._fields + and station.x_fc_authorised_user_ids): + users = station.x_fc_authorised_user_ids + + # has_pin needs sudo-read on the hash field + clocked_ids = set() + if 'hr.employee' in env and hasattr( + env['hr.employee'], '_fp_clocked_in_user_ids', + ): + clocked_ids = env['hr.employee']._fp_clocked_in_user_ids() or set() + + users_sorted = users.sorted('name') + users_sudo = users_sorted.sudo() + tiles = [] + for u, u_sudo in zip(users_sorted, users_sudo): + tiles.append({ + 'user_id': u.id, + 'name': u.name, + 'avatar_url': f'/web/image/res.users/{u.id}/avatar_128', + 'is_clocked_in': u.id in clocked_ids, + 'has_pin': bool(u_sudo.x_fc_tablet_pin_hash), + }) + # Clocked-in first, then alphabetical within bucket + tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name'])) + return {'ok': True, 'tiles': tiles} + + # ====================================================================== + # /fp/tablet/ping — heartbeat used by the OWL component on every action + # ====================================================================== + @http.route('/fp/tablet/ping', type='jsonrpc', auth='user') + def ping(self, current_tech_id=None): + """Lightweight heartbeat. Used by the OWL component to confirm + the server-side session is alive AND to log the tech-of-record + every few minutes so the server has forensic visibility into + which tech was 'driving' the tablet at any moment. + """ + if current_tech_id: + _logger.debug( + "Tablet ping: session uid %s carrying tablet_tech_id=%s", + request.env.uid, current_tech_id, + ) + return {'ok': True, 'server_time': fields.Datetime.now().isoformat()} diff --git a/fusion_plating/fusion_plating_shopfloor/tests/test_tablet_pin.py b/fusion_plating/fusion_plating_shopfloor/tests/test_tablet_pin.py index 9cfb1818..3ea8ac36 100644 --- a/fusion_plating/fusion_plating_shopfloor/tests/test_tablet_pin.py +++ b/fusion_plating/fusion_plating_shopfloor/tests/test_tablet_pin.py @@ -181,3 +181,36 @@ class TestTabletUnlock(HttpCase): res = self._unlock('1234') self.assertFalse(res['ok']) self.assertTrue(res.get('needs_setup')) + + +@tagged('-at_install', 'post_install', 'fp_shopfloor', 'fp_tablet_pin') +class TestTabletTiles(HttpCase): + """P6.1.4 — /fp/tablet/tiles endpoint.""" + + def setUp(self): + super().setUp() + self.authenticate("admin", "admin") + self.op_group = self.env.ref('fusion_plating.group_fusion_plating_operator') + self.alice = self.env['res.users'].create({ + 'name': 'Alice Tile', 'login': 'alice_tile@example.com', + 'group_ids': [(6, 0, [self.op_group.id])], + }) + self.bob = self.env['res.users'].create({ + 'name': 'Bob Tile', 'login': 'bob_tile@example.com', + 'group_ids': [(6, 0, [self.op_group.id])], + }) + self.alice.sudo().set_tablet_pin('1111') + + def test_tiles_returns_all_operators_without_station(self): + res = _rpc(self, '/fp/tablet/tiles') + self.assertTrue(res['ok']) + names = [t['name'] for t in res['tiles']] + self.assertIn('Alice Tile', names) + self.assertIn('Bob Tile', names) + + def test_tile_has_pin_flag(self): + res = _rpc(self, '/fp/tablet/tiles') + alice_tile = next(t for t in res['tiles'] if t['name'] == 'Alice Tile') + bob_tile = next(t for t in res['tiles'] if t['name'] == 'Bob Tile') + self.assertTrue(alice_tile['has_pin']) + self.assertFalse(bob_tile['has_pin'])