<reasoning_content>
*   Goal: Implement a basic `JobQueue` class in Python.
    *   Methods:
        *   `add_job(job_id: str, data: dict) -> str`: Add a job, return the `job_id`.
        *   `get_result(job_id: str) -> dict | None`: Get result of a completed job.
    *   Logic:
        *   FIFO order.
        *   Internal storage for jobs.
        *   Jobs are "processed" (the prompt implies a mechanism to handle them, though the requirements for a worker thread or background processing aren't explicitly detailed in the method signatures, but usually, a job queue implies an execution component. However, the requirement says "Each job has a unique job_id...". Let's look closer at the requirement "Jobs should be processed in FIFO order").
    *   Constraints: "Write only the Python code, no explanations."

    *   Since the prompt asks for `add_job` and `get_result`, but doesn't explicitly define a `process_jobs()` method or a background thread, I need to decide how "processing" happens.
    *   Common pattern for a simple exercise:
        1.  `add_job` puts job in a queue.
        2.  A mechanism (like a `process` method or a background thread) moves them from queue to a "results" dictionary.
    *   Wait, the requirement says: "Jobs should be processed in FIFO order". If I just add jobs, they stay in the queue. I need a way to *execute* them to make `get_result` meaningful.
    *   However, looking at the example usage:
        ```python
        queue = JobQueue()
        job_id = queue.add_job("job1", {"task": "process_data"})
        # After processing...
        result = queue.get_result("job1")
        ```
        The comment `# After processing...` suggests processing happens outside the call to `add_job`.
    *   I'll implement a simple version where I provide a way to process them (maybe a `process_next` method or a loop), or I'll implement a simple worker mechanism.
    *   Actually, a strictly minimal interpretation:
        *   `add_job` stores job in a `collections.deque`.
        *   A `process_jobs` method (or similar) simulates work.
        *   `get_result` retrieves from a `results` dict.

    Let's refine the class structure:
    - `self.queue = deque()`
    - `self.results = {}`
    - `self.jobs = {}` (to track if a job exists)

    Wait, the prompt is a bit ambiguous on *how* they get processed. If I don't include a way to process, the `get_result` will always return `None`.
    Let's add a `process_next_job` method to make it functional, or just use a simple internal processing mechanism.
    Actually, a common way to handle "Basic job queue" is to have a queue of tasks and a results store.

    Let's implement:
    - `add_job`: Appends to a `collections.deque`.
    - `process_one`: (Internal or public) Takes a job from deque, performs "task", stores result.
    - `get_result`: Returns value from results map.

    Let's look at the prompt again. "Jobs should be processed in FIFO order". "The queue should store jobs internally until they are processed".
    If I want to make the example usage work, I should probably include a method to simulate processing or just assume the caller knows they need to trigger processing. But since I'm writing the whole class, I'll include a `process_jobs` or `run` method, or maybe just `process_job`.

    Actually, let's make it so `add_job` just adds to the queue, and I provide a `process_job` method to satisfy the "processed" requirement.

    *Self-Correction:* The prompt is a coding challenge. Often in these, if they don't specify the worker, they expect the `JobQueue` to be the container.
    Let's provide a `process_jobs` method to make it a complete, testable unit.

    Wait, let's try to match the signature exactly.
    `add_job(job_id, data)` -> returns `job_id`.
    `get_result(job_id)` -> returns `dict | None`.

    To make the "FIFO" requirement meaningful, I'll use `collections.deque`.

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

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            self._queue.append((job_id, data))
            return job_id

        def process_next_job(self):
            if not self._queue:
                return
            job_id, data = self._queue.popleft()
            # Simulate processing: just returning the data or a success msg
            # In a real scenario, this would execute logic.
            # For this exercise, let's just treat the data as the task.
            self._results[job_id] = {"status": "completed", "output": data}

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

    But wait, if I add `process_next_job`, the user has to call it. The prompt doesn't ask for it, but it's required for the "processed" part to exist. Or, I could use a background thread. But for a "basic" implementation, a manual process method or a `process_all` is cleaner.

    Actually, I'll implement a simple `process_job` method that the user can call to simulate the "After processing..." step in the example.

    1. `collections.deque` for FIFO.
    2. `dict` for results.
    3. `add_job` puts (id, data) in deque.
    4. `process_job` (optional but helpful) pops from deque and puts in results.
    5. `get_result` checks results.

    Wait, looking at the example again:
    `job_id = queue.add_job("job1", {"task": "process_data"})`
    `# After processing...`
    `result = queue.get_result("job1")`

    It doesn't show how the processing happens. I will include a `process_jobs` method so the code is actually runnable and demonstrates the logic.

    Let's make sure the code is clean.

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

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            self._queue.append((job_id, data))
            return job_id

        def process_all(self):
            """Helper to process all jobs in the queue for demonstration."""
            while self._queue:
                job_id, data = self._queue.popleft()
                # Simulating job execution
                self._results[job_id] = {"status": "success", "data": data}

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

    Actually, a more "correct" way for a "Job Queue" interface usually includes a `process` or `run` mechanism. I'll stick to the requirements strictly but add a `process_next` to ensure the FIFO logic is testable.

    Actually, I'll just implement `add_job` and `get_result` as requested, and a `process_next` to allow the user to fulfill the "After processing" comment in their own logic.

    Wait, if the requirement says "Jobs should be processed in FIFO order", it implies the *logic* of the queue handles the order.

    Let's refine.

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

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            self._queue.append((job_id, data))
            return job_id

        def process_next(self) -> None:
            if self._queue:
                job_id, data = self._queue.popleft()
                # Simulate task execution
                self._results[job_id] = {"result": f"processed_{data.get('task', '')}"}

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

    I will include `process_next` because without a way to move items from the queue to the results, `get_result` is useless.

    One more detail: The prompt says: "Each job has a unique job_id that is returned when added". This is handled.

    Final plan:
    - `collections.deque` for FIFO queue.
    - `dict` for storage/results.
    - `add_job` method.
    - `get_result` method.
    - `process_next` method (to enable the FIFO logic to be demonstrated).

    Actually, I'll just provide the class as requested. I'll add a `run_next` method.

    Wait, I should check if there is a way to make it "automatic" (background thread). A background thread makes it a "real" job queue. But for a "basic" implementation, manual processing is safer and less prone to threading complexity/leaks in a simple coding task unless requested.

    I will stick to the requested methods and add a `process_jobs` method to make it complete.

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

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            self._queue.append((job_id, data))
            return job_id

        def process_next(self) -> None:
            """Processes the next job in the FIFO queue."""
            if self._queue:
                job_id, data = self._queue.popleft()
                # Simulated processing logic
                self._results[job_id] = {"status": "completed", "data": data}

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

    Wait, what if the user wants to pass a function? The prompt says `data: dict`. So the "processing" is abstract. I'll just simulate it.

    Actually, to be as helpful as possible while following "Write only the Python code", I'll implement the class.

    *Self-Correction*: If I only provide `add_job` and `get_result`, the class is technically incomplete for the user to *run* their example unless they implement the middle part. But the prompt is "Implement a basic job queue". A job queue usually has a way to work. I'll include a `process_jobs` method.

    Final Code Structure:
    ```python
    from collections import deque
    from typing import Dict, Any, Optional

    class JobQueue:
        def __init__(self):
            self._queue = deque()
            self._results = {}

        def add_job(self, job_id: str, data: dict) -> str:
            self._queue.append((job_id, data))
            return job_id

        def process_next(self) -> None:
            if self._queue:
                job_id, data = self._queue.popleft()
                # Simulate processing: in a real system, this would execute a task.
                self._results[job_id] = {"status": "success", "processed_data": data}

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

    Wait, one detail: "The queue should store jobs internally until they are processed". My `_queue` does this. "Jobs should be processed in FIFO order". `popleft()` does this.

    Let's write the code.
</reasoning_content>

<content>
```python
from collections import deque
from typing import Dict, Any, Optional

class JobQueue:
    def __init__(self):
        self._queue = deque()
        self._results = {}

    def add_job(self, job_id: str, data: dict) -> str:
        """Adds a job to the FIFO queue."""
        self._queue.append((job_id, data))
        return job_id

    def process_next(self) -> None:
        """
        Processes the next job in the queue.
        In a real-world scenario, this would involve executing task logic.
        """
        if self._queue:
            job_id, data = self._queue.popleft()
            # Simulate processing by returning the data back with a status
            self._results[job_id] = {
                "status": "completed",
                "original_data": data
            }

    def get_result(self, job_id: str) -> Optional[Dict[str, Any]]:
        """Returns the result of a completed job, or None if not found/not completed."""
        return self._results.get(job_id)

# Example usage:
if __name__ == "__main__":
    queue = JobQueue()
    
    # Add jobs
    id1 = queue.add_job("job1", {"task": "process_data"})
    id2 = queue.add_job("job2", {"task": "send_email"})
    
    # Process jobs in FIFO order
    queue.process_next()  # Processes job1
    queue.process_next()  # Processes job2
    
    # Get results
    print(f"Result 1: {queue.get_result(id1)}")
    print(f"Result 2: {queue.get_result(id2)}")
    print(f"Result 3 (non-existent): {queue.get_result('job3')}")
```
</content>