from collections import deque
from typing import Dict, Optional

class JobQueue:
    def __init__(self):
        self._queue: deque = deque()
        self._jobs: Dict[str, dict] = {}
        self._results: Dict[str, dict] = {}
        self._processed: set = set()

    def add_job(self, job_id: str, data: dict) -> str:
        if job_id in self._processed:
            raise ValueError(f"Job {job_id} has already been processed")
        self._queue.append(job_id)
        self._jobs[job_id] = data
        return job_id

    def get_result(self, job_id: str) -> Optional[dict]:
        return self._results.get(job_id)

    def process(self):
        while self._queue:
            job_id = self._queue.popleft()
            if job_id in self._jobs and job_id not in self._processed:
                data = self._jobs[job_id]
                self._results[job_id] = {"status": "completed", "data": data}
                self._processed.add(job_id)

    def process_until_completed(self, job_id: str):
        """Helper to ensure a specific job is processed before retrieval."""
        while job_id not in self._processed:
            self.process()
            if not self._queue:
                # If queue is empty but job not processed, it means job_id was never added
                break
        return self._results.get(job_id)

# Basic implementation focusing on the requested methods and FIFO logic.
# The `process` method is included to fulfill the requirement that jobs are "processed".
```python
from collections import deque
from typing import Dict, Optional

class JobQueue:
    def __init__(self):
        self._queue: deque = deque()
        self._jobs: Dict[str, dict] = {}
        self._results: Dict[str, dict] = {}

    def add_job(self, job_id: str, data: dict) -> str:
        if job_id in self._results:
            return job_id  # Already processed
        
        self._queue.append(job_id)
        self._jobs[job_id] = data
        return job_id

    def get_result(self, job_id: str) -> Optional[dict]:
        return self._results.get(job_id)

    def _process_job(self, job_id: str):
        if job_id in self._jobs and job_id not in self._results:
            data = self._jobs[job_id]
            self._results[job_id] = {"status": "completed", "data": data}
            del self._jobs[job_id]

    def process(self):
        while self._queue:
            job_id = self._queue.popleft()
            self._process_job(job_id)
```

I'll provide the code now. I will remove the `process_until_completed` helper to keep it strictly to the requirements + necessary internal logic for `process` to work, as `process` is necessary to satisfy "until they are processed" given the example usage implies processing happens. I will include `process` method to make the queue functional.

Wait, I should check if I should include `process`. The prompt asks for `add_job` and `get_result`.
If I don't include `process`, the queue never processes.
I will include `process`.

One correction: The prompt example shows `# After processing...`.
If I don't provide a way to process, the example is pseudocode.
I will add `process` method.

Final check on constraints: "Write only the Python code".
I will output the code block.

One detail: `job_id` uniqueness. The prompt says "Each job has a unique job_id that is returned when added". Since `add_job` takes `job_id`, I assume the caller ensures uniqueness or I should check. I'll assume uniqueness as per signature.

Wait, I'll refine the `process` method to not require it to be public if not asked? No, it needs to be called. I'll make it public.

Wait, I'll try to make it so `get_result` can trigger processing if needed? No, that's not a queue.
Okay, `process` it is.

Wait, I'll check the typing. `dict | None` is Python 3.10+. I'll use it.

Wait, I'll check the `add_job` return. It returns `job_id`.
Okay.

Wait, I'll check if I need to handle `job_id` collision.
I'll assume valid input.

Okay, ready to write.

Wait, I'll remove the `process_until_completed` helper.
I'll keep it simple.

Wait, I'll ensure I don't use `threading` unless necessary. `process` method is simpler.

Wait, I'll check if I should put `process` inside the class or outside? Inside.

Wait, I'll check if I should use `typing` imports. `from typing import Optional` is good practice.

Wait, I'll check the `get_result` return type. `dict | None`.

Okay.

```python
from collections import deque
from typing import Dict, Optional

class JobQueue:
    def __init__(self):
        self._queue: deque = deque()
        self._jobs: Dict[str, dict] = {}
        self._results: Dict[str, dict] = {}

    def add_job(self, job_id: str, data: dict) -> str:
        if job_id not in self._queue and job_id not in self._results:
            self._queue.append(job_id)
            self._jobs[job_id] = data
        return job_id

    def get_result(self, job_id: str) -> Optional[dict]:
        return self._results.get(job_id)

    def process(self):
        while self._queue:
            job_id = self._queue.popleft()
            if job_id in self._jobs:
                data = self._jobs[job_id]
                self._results[job_id] = {"status": "completed", "data": data}
                del self._jobs[job_id]
```
This looks solid. I will remove the check in `add_job` to strictly follow "Add a job". If added again, usually queue appends or ignores. I'll allow adding if not processed.
Actually, to keep it "basic", I'll just append.

Wait, I'll check if I should handle the `job_id` being already in queue.
I'll just append. It's a queue.
Wait, `add_job` returns `job_id`.
Okay.

Wait, I'll check if I need to import `typing`.
I'll use `Optional` or just `| None`.
I'll use `| None` as in the prompt.

Wait, I'll check if I need to put the code in a markdown block.
"Write only the Python code".
I will put it in a markdown block.

Wait, I'll check if I need to handle the `process` method in the requirements.
The requirements list specific methods. I will add `process` to make it work, as a queue without processing is not a queue.
Okay.

One last check: `get_result` returns `dict | None`.
My code returns `self._results.get(job_id)`.
If key not found, returns `None`.
If found, returns value (dict).
Correct.

`add_job` returns `str`.
Correct.

FIFO: `deque` used.
Correct.

Internal storage: `self._jobs`.
Correct.

Unique job_id: Returned when added.
Correct.

Okay, generating output.
</think>

from collections import deque
from typing import Dict, Optional

class JobQueue:
    def __init__(self):
        self._queue: deque = deque()
        self._jobs: Dict[str, dict] = {}
        self._results: Dict[str, dict] = {}

    def add_job(self, job_id: str, data: dict) -> str:
        if job_id not in self._results:
            self._queue.append(job_id)
            self._jobs[job_id] = data
        return job_id

    def get_result(self, job_id: str) -> Optional[dict]:
        return self._results.get(job_id)

    def process(self):
        while self._queue:
            job_id = self._queue.popleft()
            if job_id in self._jobs:
                data = self._jobs[job_id]
                self._results[job_id] = {"status": "completed", "data": data}
                del self._jobs[job_id]