FIX_HRAPI_IR_DOCS_UPLOAD
This commit is contained in:
parent
6cd89bbf2f
commit
b81ed82511
@ -609,7 +609,7 @@ $routes->group("employeeRest/api/v1", ["filter" => ['ratelimit', 'authJWT']], fu
|
|||||||
$routes->post('create', 'Api\NonEbClaimApiController::createClaim');
|
$routes->post('create', 'Api\NonEbClaimApiController::createClaim');
|
||||||
$routes->post('list', 'Api\NonEbClaimApiController::listClaims');
|
$routes->post('list', 'Api\NonEbClaimApiController::listClaims');
|
||||||
$routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
|
$routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
|
||||||
$routes->post('(:num)/upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc/$1');
|
$routes->post('upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc');
|
||||||
$routes->get('statuses', 'Api\NonEbClaimApiController::listClaimStatuses');
|
$routes->get('statuses', 'Api\NonEbClaimApiController::listClaimStatuses');
|
||||||
$routes->post('policies', 'Api\NonEbClaimApiController::listPolicies');
|
$routes->post('policies', 'Api\NonEbClaimApiController::listPolicies');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -653,7 +653,61 @@ class NonEbClaimApiController extends BaseController
|
|||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function uploadRequiredDoc(int $claim_id)
|
public function uploadRequiredDoc()
|
||||||
|
{
|
||||||
|
$authUser = $this->getAuthUser();
|
||||||
|
if (!$authUser) {
|
||||||
|
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket_id = $this->request->getPost('ticket_id') ?? null;
|
||||||
|
$get_file_data = $this->request->getFiles('claim_docs') ?? null;
|
||||||
|
$get_docs_name = $this->request->getPost('claim_doc_names') ?? [];
|
||||||
|
$required_docs = $this->request->getPost('required_docs') ?? null;
|
||||||
|
|
||||||
|
if (is_string($get_docs_name)) {
|
||||||
|
$decoded = json_decode($get_docs_name, true);
|
||||||
|
$get_docs_name = json_last_error() === JSON_ERROR_NONE ? $decoded : [];
|
||||||
|
} elseif (!is_array($get_docs_name)) {
|
||||||
|
$get_docs_name = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_data = [];
|
||||||
|
if (!empty($get_file_data)) {
|
||||||
|
$file_path = WRITEPATH . 'uploads/claim_files/';
|
||||||
|
$file_data = multi_file_Upload($get_file_data, $file_path, $get_docs_name, UPLOAD_EXT_CLAIM_DOCS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($file_data) && !empty($ticket_id)) {
|
||||||
|
foreach ($file_data as $value) {
|
||||||
|
$this->claimFilesModel->insert([
|
||||||
|
'ticket_id' => $ticket_id,
|
||||||
|
'ticket_type' => 2,
|
||||||
|
'doc_name' => $value['doc_name'],
|
||||||
|
'file_name' => $value['file_name'],
|
||||||
|
'url' => $value['file_path'],
|
||||||
|
'file_type' => 2,
|
||||||
|
'mime_type' => getMimeTypeByFileName($value['file_name']),
|
||||||
|
'docs_for_ir' => 1,
|
||||||
|
'is_active' => 1,
|
||||||
|
'created_by' => self::API_SYSTEM_USER_ID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
db_connect()->query(
|
||||||
|
'UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?',
|
||||||
|
[$required_docs, $ticket_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Docs uploaded. ticket_id: $ticket_id, count: " . count($file_data) . ", user: {$authUser['id']}");
|
||||||
|
|
||||||
|
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Files uploaded successfully'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Failed to upload the file'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadRequiredDoc_v1(int $claim_id)
|
||||||
{
|
{
|
||||||
$authUser = $this->getAuthUser();
|
$authUser = $this->getAuthUser();
|
||||||
if (!$authUser) {
|
if (!$authUser) {
|
||||||
|
|||||||
@ -186,13 +186,19 @@ class LeadsModel extends Model
|
|||||||
public function getLeadForInsertClientList($type = null, $client_id = null)
|
public function getLeadForInsertClientList($type = null, $client_id = null)
|
||||||
{
|
{
|
||||||
$query = $this->db->table('leads')
|
$query = $this->db->table('leads')
|
||||||
->select('leads.*, user_profiles.first_name as user_name')
|
->select('leads.*, user_profiles.first_name as user_name, policy_type.allocg')
|
||||||
->join('user_profiles', 'leads.created_by = user_profiles.id')
|
->join('user_profiles', 'leads.created_by = user_profiles.id')
|
||||||
|
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
|
||||||
->where('leads.is_active', 1)
|
->where('leads.is_active', 1)
|
||||||
->where('leads.status', 'won')
|
->where('leads.status', 'won')
|
||||||
->where("leads.proposel_data IS NOT NULL AND leads.proposel_data <> ''")
|
->groupStart()
|
||||||
->where("(leads.is_client_created = '' OR leads.is_client_created IS NULL)")
|
->where("(leads.is_client_created = '' OR leads.is_client_created IS NULL)")
|
||||||
->where("(leads.is_policy_created = '' OR leads.is_policy_created IS NULL)");
|
->orWhere("(leads.is_policy_created = '' OR leads.is_policy_created IS NULL)")
|
||||||
|
->groupEnd()
|
||||||
|
->groupStart()
|
||||||
|
->where("policy_type.allocg != 'EB'")
|
||||||
|
->orWhere("(policy_type.allocg = 'EB' AND leads.proposel_data IS NOT NULL AND leads.proposel_data <> '')")
|
||||||
|
->groupEnd();
|
||||||
|
|
||||||
if ($type) {
|
if ($type) {
|
||||||
$query->where('leads.lead_type', $type);
|
$query->where('leads.lead_type', $type);
|
||||||
|
|||||||
@ -357,7 +357,7 @@ input:checked + .slider_blue::before {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
||||||
<button type="submit" class="btn btn-primary" onclick="featchClient()">Fetch Client</button>
|
<button type="submit" class="btn btn-primary" onclick="featchClient()">Fetch Data</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
|
|||||||
@ -236,7 +236,7 @@ table.dataTable tbody td {
|
|||||||
<a href="#" class="dropdown-item btnEdit2 btnRfqSheetList" data-id="<?php echo $row['id']; ?>">
|
<a href="#" class="dropdown-item btnEdit2 btnRfqSheetList" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
||||||
</a>
|
</a>
|
||||||
<?php if (! in_array($row['status'], ['queued', 'rfq_created', 'rfq_sent'])) {?>
|
<?php if (! in_array($row['status'], ['queued', 'rfq_created'])) {?>
|
||||||
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
||||||
<a href="#" class="dropdown-item btnEdit3 btnQcrSheetList" data-id="<?php echo $row['id']; ?>">
|
<a href="#" class="dropdown-item btnEdit3 btnQcrSheetList" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
||||||
@ -262,7 +262,7 @@ table.dataTable tbody td {
|
|||||||
<a href="<?php echo base_url('/rfq/list/') . $row['id'] . '/' . 1; ?>" class="dropdown-item btnEdit2" data-id="<?php echo $row['id']; ?>">
|
<a href="<?php echo base_url('/rfq/list/') . $row['id'] . '/' . 1; ?>" class="dropdown-item btnEdit2" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
||||||
</a>
|
</a>
|
||||||
<?php if (! in_array($row['status'], ['queued', 'rfq_created', 'rfq_sent'])) {?>
|
<?php if (! in_array($row['status'], ['queued', 'rfq_created'])) {?>
|
||||||
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
||||||
<a href="<?php echo base_url('/rfq/list/') . $row['id'] . '/' . 2; ?>" class="dropdown-item btnEdit3" data-id="<?php echo $row['id']; ?>">
|
<a href="<?php echo base_url('/rfq/list/') . $row['id'] . '/' . 2; ?>" class="dropdown-item btnEdit3" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
||||||
|
|||||||
@ -45,3 +45,29 @@ Also removed early-return on empty history so all 4 keys are always present in t
|
|||||||
|
|
||||||
### API Docs
|
### API Docs
|
||||||
- `dev_logs/non_eb_claim_api.md`
|
- `dev_logs/non_eb_claim_api.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Change: `uploadRequiredDoc` — rewritten to mirror `uploadIRDocs`
|
||||||
|
|
||||||
|
**Old URL:** `POST /api/v1/non-eb-claim/{claim_id}/upload-required-doc`
|
||||||
|
**New URL:** `POST /api/v1/non-eb-claim/upload-required-doc`
|
||||||
|
|
||||||
|
Old function copied as `uploadRequiredDoc_v1` (preserved for reference).
|
||||||
|
|
||||||
|
**What changed:**
|
||||||
|
- Removed `claim_id` from URL; `ticket_id` now comes from POST body
|
||||||
|
- Switched from single-file (`file` field) to multi-file (`claim_docs[]` field) using `multi_file_Upload`
|
||||||
|
- Added `claim_doc_names[]` — index-matched names for uploaded files
|
||||||
|
- `required_docs` JSON string now sent by client and stored directly to `non_eb_ticket_master.required_docs` (no server-side checklist merging)
|
||||||
|
- Each inserted `claim_files` row now has `docs_for_ir = 1`
|
||||||
|
- Removed TPA push (`pushClaimFiles` not called)
|
||||||
|
- Success response simplified to `{status, code, message}`
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `app/Controllers/Api/NonEbClaimApiController.php`
|
||||||
|
- `app/Config/Routes.php`
|
||||||
|
|
||||||
|
### API Docs
|
||||||
|
- `nonebapidocs.md`
|
||||||
|
- `dev_logs/non_eb_claim_api.md`
|
||||||
|
|||||||
@ -195,45 +195,34 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Upload Required Document
|
## 4. Upload Required IR Documents
|
||||||
`POST /{claim_id}/upload-required-doc`
|
`POST /upload-required-doc`
|
||||||
|
|
||||||
**Content-Type:** `multipart/form-data`
|
**Content-Type:** `multipart/form-data`
|
||||||
|
|
||||||
### Request Fields
|
### Request Fields
|
||||||
| Field | Type | Required | Notes |
|
| Field | Type | Required | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `document_name` | string | Yes | Must exactly match a name in `required_docs.docs` |
|
| `ticket_id` | integer | Yes | The non-EB claim (ticket) ID |
|
||||||
| `file` | file | Yes | Allowed: pdf, jpg, jpeg, png, doc, docx, xls, xlsx |
|
| `claim_docs[]` | file(s) | Yes | One or more files to upload |
|
||||||
|
| `claim_doc_names[]` | string[] | Yes | Name for each file — index-matched to `claim_docs[]` |
|
||||||
|
| `required_docs` | string (JSON) | Yes | Full updated checklist JSON to store (e.g. `{"is_action_freeze":false,"docs":[...]}`) |
|
||||||
|
|
||||||
### Response — 200
|
### Response — 200
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": true,
|
"status": true,
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"message": "Document uploaded successfully",
|
"message": "Files uploaded successfully"
|
||||||
"claim_id": 42,
|
|
||||||
"document_name": "Claim Form",
|
|
||||||
"download_url": "https://yourdomain.com/downloadClaimFile/101",
|
|
||||||
"required_docs": {
|
|
||||||
"is_action_freeze": false,
|
|
||||||
"docs": [
|
|
||||||
{ "document_name": "Claim Form", "document_received": true },
|
|
||||||
{ "document_name": "Survey Report", "document_received": false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Responses
|
### Error Responses
|
||||||
| Code | Reason |
|
| Code | Reason |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 400 | Missing `document_name` or invalid file |
|
| 400 | No files uploaded or `ticket_id` missing |
|
||||||
| 404 | Claim not found, or `document_name` not in checklist |
|
| 401 | Missing / expired token |
|
||||||
| 415 | Unsupported file type |
|
| 500 | File upload or DB failed |
|
||||||
| 422 | No required docs checklist configured |
|
|
||||||
| 423 | Checklist is locked (`is_action_freeze = true`) |
|
|
||||||
| 500 | File upload or DB transaction failed |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ All responses follow this consistent shape:
|
|||||||
| 1 | `POST` | `/api/v1/non-eb-claim/create` | Raise a new Non-EB claim |
|
| 1 | `POST` | `/api/v1/non-eb-claim/create` | Raise a new Non-EB claim |
|
||||||
| 2 | `POST` | `/api/v1/non-eb-claim/list` | List / search claims |
|
| 2 | `POST` | `/api/v1/non-eb-claim/list` | List / search claims |
|
||||||
| 3 | `GET` | `/api/v1/non-eb-claim/history/{claim_id}` | Status timeline of a claim |
|
| 3 | `GET` | `/api/v1/non-eb-claim/history/{claim_id}` | Status timeline of a claim |
|
||||||
| 4 | `POST` | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a required document |
|
| 4 | `POST` | `/api/v1/non-eb-claim/upload-required-doc` | Upload required IR documents for a claim |
|
||||||
| 5 | `GET` | `/api/v1/non-eb-claim/statuses` | List Non-EB claim statuses |
|
| 5 | `GET` | `/api/v1/non-eb-claim/statuses` | List Non-EB claim statuses |
|
||||||
| 6 | `POST` | `/api/v1/non-eb-claim/policies` | List Non-EB policies by client (MD5) + branch |
|
| 6 | `POST` | `/api/v1/non-eb-claim/policies` | List Non-EB policies by client (MD5) + branch |
|
||||||
|
|
||||||
@ -319,23 +319,13 @@ Authorization: Bearer <token>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Upload Required Document
|
## 4. Upload Required IR Documents
|
||||||
|
|
||||||
**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
|
**POST** `/api/v1/non-eb-claim/upload-required-doc`
|
||||||
|
|
||||||
### How it works
|
### How it works
|
||||||
|
|
||||||
Each claim has a **required documents checklist** configured by the staff (e.g. "Invoice Copy", "Survey Report"). This endpoint lets the user upload a file against one of those checklist items.
|
Uploads one or more IR documents for a claim. Each file is stored in `claim_files` with `docs_for_ir = 1`. The full updated `required_docs` JSON (checklist) sent by the client is written directly to `non_eb_ticket_master.required_docs` — the caller is responsible for reflecting any `document_received` flag changes in the JSON before sending.
|
||||||
|
|
||||||
When a document is uploaded successfully, its `document_received` flag in the checklist is set to `true` and the updated checklist is returned so you can refresh the UI.
|
|
||||||
|
|
||||||
The `document_name` you send must **exactly match** (case-sensitive) one of the `document_name` values in the checklist. Use the checklist data to drive your UI — display the exact names as options so the user cannot type the wrong value.
|
|
||||||
|
|
||||||
### URL Parameter
|
|
||||||
|
|
||||||
| Param | Type | Required | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `claim_id` | integer | Yes | The claim to upload against |
|
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
|
|
||||||
@ -343,37 +333,24 @@ The `document_name` you send must **exactly match** (case-sensitive) one of the
|
|||||||
|
|
||||||
| Field | Required | Type | Notes |
|
| Field | Required | Type | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `document_name` | Yes | string | Must exactly match a `document_name` in the claim's checklist |
|
| `ticket_id` | Yes | integer | The non-EB claim (ticket) ID |
|
||||||
| `file` | Yes | file | Allowed types: pdf, jpg, jpeg, png, doc, docx, xls, xlsx |
|
| `claim_docs[]` | Yes | file(s) | One or more files; use the same field name for multiple |
|
||||||
|
| `claim_doc_names[]` | Yes | string[] | Document name for each uploaded file — index-matched to `claim_docs[]` |
|
||||||
### How to get the checklist
|
| `required_docs` | Yes | string (JSON) | Full checklist JSON to persist (e.g. `{"is_action_freeze":false,"docs":[...]}`) |
|
||||||
|
|
||||||
The required documents list for a claim is returned when you fetch claim details (or can be shown after claim creation). The structure is:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"is_action_freeze": false,
|
|
||||||
"docs": [
|
|
||||||
{ "document_name": "Invoice Copy", "document_received": false },
|
|
||||||
{ "document_name": "Survey Report", "document_received": true },
|
|
||||||
{ "document_name": "Police FIR Copy","document_received": false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `is_action_freeze: true` means the checklist is locked — the upload endpoint will reject new uploads with `423`.
|
|
||||||
- `document_received: true` means the staff has already received this document. You may still re-upload if needed (previous upload is replaced).
|
|
||||||
- Show only the document names as upload targets — do not allow freetext entry.
|
|
||||||
|
|
||||||
### Example Request
|
### Example Request
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/v1/non-eb-claim/123/upload-required-doc
|
POST /api/v1/non-eb-claim/upload-required-doc
|
||||||
Content-Type: multipart/form-data
|
Content-Type: multipart/form-data
|
||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
document_name = Invoice Copy
|
ticket_id = 3
|
||||||
file = <file>
|
claim_docs[] = <file1>
|
||||||
|
claim_docs[] = <file2>
|
||||||
|
claim_doc_names[] = ecard
|
||||||
|
claim_doc_names[] = Sample Doc 1
|
||||||
|
required_docs = {"is_action_freeze":false,"docs":[{"document_name":"Sample Doc 1","document_received":true},{"document_name":"Sample Doc 2","document_received":false}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Success Response `200`
|
### Success Response `200`
|
||||||
@ -382,36 +359,16 @@ file = <file>
|
|||||||
{
|
{
|
||||||
"status": true,
|
"status": true,
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"message": "Document uploaded successfully",
|
"message": "Files uploaded successfully"
|
||||||
"claim_id": 123,
|
|
||||||
"document_name": "Invoice Copy",
|
|
||||||
"download_url": "https://yourdomain.com/downloadClaimFile/456",
|
|
||||||
"required_docs": {
|
|
||||||
"is_action_freeze": false,
|
|
||||||
"docs": [
|
|
||||||
{ "document_name": "Invoice Copy", "document_received": true },
|
|
||||||
{ "document_name": "Survey Report", "document_received": true },
|
|
||||||
{ "document_name": "Police FIR Copy", "document_received": false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Use the returned `required_docs` to update the checklist UI immediately without a separate fetch.
|
|
||||||
|
|
||||||
### Error Responses
|
### Error Responses
|
||||||
|
|
||||||
| Code | Scenario | Message |
|
| Code | Scenario | Message |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `400` | `document_name` not sent | `"document_name is required"` |
|
| `400` | No valid files uploaded or `ticket_id` missing | `"Failed to upload the file"` |
|
||||||
| `400` | No file sent or invalid file | `"A valid file is required"` |
|
|
||||||
| `401` | Missing / expired token | `"Unauthorized"` |
|
| `401` | Missing / expired token | `"Unauthorized"` |
|
||||||
| `404` | Claim not found | `"Claim not found"` |
|
|
||||||
| `404` | `document_name` not in checklist | `"Document 'Invoice Copy' not found in required documents list"` |
|
|
||||||
| `415` | Unsupported file type | `"Unsupported file type: bmp"` |
|
|
||||||
| `422` | Claim has no checklist configured | `"No required documents checklist configured for this claim"` |
|
|
||||||
| `423` | Checklist is locked | `"Document checklist is locked for this claim"` |
|
|
||||||
| `500` | Upload or DB failure | `"File upload failed"` / `"Failed to save document. Please try again."` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user