As you've spend the whole day on this, let me give you a bunch of pointers. I hope you can learn something from it and understand why those "Undefined" and "Notices" are there. I've not tested the code, but if you go thru all the comments I hope you'll understand and will be able to make it getting to work.
<?php
    // Always start sessions first:
    // if your db.php throws an error, the session can't start anymore
    // and throws a warning
    session_start(); 
    require('db.php');
    // Do check if a variable exists. PHP should throw you a warning otherwise.
    if(empty($_SESSION['logged_in']) || $_SESSION['logged_in'] != 1) {
        $_SESSION['message'] = "You must log in before viewing your profile page!";
        header("Location: error.php");
        // Stop running the script after a redirect! 
        // A header is an instruction, a client 
        // might simple ignore it and show the page content anyway
        exit();
    }
    // Check if the variable exists!
    // Long way:
    // if(isset($_POST['stid'])) { $postedId = $_POST['stid'] } else { $postedId = false; }
    // Medium way:
    // $postedId = (isset($_POST['stid']) ? $_POST['stid'] : false;
    // Short way:
    $postedId = $_POST['stid']?:false;
    // Make your query look nice, makes your life easy and debugging too
    // Query questions: 
    // 1. What if $_SESSION['tcid'] doesn't exist?
    // 2. What if $postedId doesn't exist?
    // 3. What if $postedId is 0; DROP TABLE students; ?
    // Remember, a client can send anything via $_POST['stid']
    //
    //$qry = "SELECT stcontents.id, `st_id`, `name` 
    //      FROM `students`, `stcontents` 
    //      WHERE stcontents.tc_id = " . $_SESSION['tcid'] . " 
    //      AND students.id = st_id 
    //      AND st_id = " . $postedId . " GROUP BY st_id";
    //
    // Read about mysqli_real_escape_string
    // https://www.php.net/manual/en/mysqli.real-escape-string.php
    // Want to do it really right? Use prepared statements
    // https://www.php.net/manual/en/mysqli.prepare.php
    $qry = "SELECT stcontents.id, `st_id`, `name` 
            FROM `students`, `stcontents` 
            WHERE stcontents.tc_id = " . mysqli_real_escape_string($mysqli, $_SESSION['tcid']) . " 
            AND students.id = st_id 
            AND st_id = " . mysqli_real_escape_string($mysqli, $postedId) . " GROUP BY st_id";
    $result = mysqli_query($mysqli, $qry);
    $row = mysqli_fetch_array($result);
    // But what if no result was found?
    if(empty($postedId) || empty($row)) {
        exit('Something above went wrong!');
    }
?>
<!doctype html>
<html lang="en">
    <body>
        <div class="container" id="turma-container">
            <!-- 
            // Leave out the action if it's empty anyway
            // https://stackoverflow.com/questions/1131781/is-it-a-good-practice-to-use-an-empty-url-for-a-html-forms-action-attribute-a
            -->
            <form method="post" enctype="multipart/form-data">
                <p>Image:</p>
                <input type="file" name="fileToUpload" id="fileToUpload">
                <input type="submit" value="Upload Image" name="submit">
            </form>
            <?php
                $target_dir = "resources/images/"; // Better use the full path
                // $target_file = $target_dir."img".$postedId.".jpg";
                // What if $postedId is /../../logo ?
                // Is resources/images/img/../../logo.jpg a valid path?             
                //
                // I'll assume $postedId will be an integer (number)
                // using https://www.php.net/manual/en/function.settype.php
                settype($postedId, 'int');
                // Another approach: basename()
                // https://www.php.net/manual/en/function.basename.php
                // https://www.php.net/manual/en/features.file-upload.post-method.php
                $target_file = $target_dir . basename("img" . $postedId . ".jpg");
                $uploadOk = 1;
                // $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
                // You have just made a string $target_file.. so
                // nothing is there, or it would be jpg anyway, since you've said ".jpg"
                if(isset($_POST["submit"])) {
                    // So $_POST['submit'] might be there, but was the fileToUpload too?
                    if(empty($_FILES["fileToUpload"])) {
                        exit('no file!');
                    }
                    // $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
                    //
                    // Well although getimagesize indeed does return false on failure,
                    // read the caution "Do not use to check that a given file is a valid image."
                    // here https://www.php.net/manual/en/function.getimagesize.php
                    // the path it also incomplete ($target_dir is missing)
                    $finfo = finfo_open(FILEINFO_MIME_TYPE);
                    if($mime = finfo_file($finfo, $_FILES["fileToUpload"]["tmp_name"])) {
                        if($mime == 'image/jpg' || $mime == 'image/jpeg') {
                            // Now you could use getimagesize as extra check
                            // But there might be better alternatives
                            if(getimagesize($_FILES["fileToUpload"]["tmp_name"]) === false) {
                                echo "File is corrupt";
                                $uploadOk = 0;
                            } else {                        
                                echo "File is an image - " . $mime . ".";
                                $uploadOk = 1;
                            }                           
                        } else {
                            echo $mime . " is not supported.";
                            $uploadOk = 0;
                        }                       
                    } else {
                        echo "Invalid file";
                        $uploadOk = 0;
                    }               
                    if($_FILES["fileToUpload"]["size"] > 500000) {
                        echo "Sorry, your file is too large.";
                        $uploadOk = 0;
                    }
                    // if($imageFileType != "jpg") {
                    //  echo "Sorry, only JPG files are allowed.";
                    //  $uploadOk = 0;
                    // }
                    //
                    // Done this above.
                    // If the idea is some pre-flight check, consider $_FILES['fileToUpload']['type'] 
                    if($uploadOk == 0) {
                        echo "Sorry, your file was not uploaded.";
                    } else {
                        if(move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
                            echo "The file ". basename($_FILES["fileToUpload"]["name"]). " has been uploaded.";
                        } else {
                            echo "Sorry, there was an error uploading your file.";
                        }
                    }
                }
            ?>
        </div>
    </body>
</html>
There can still be a lot of optimization, but this should get you going. Best of luck with it! If you have some questions, feel free to comment.
After your comments, let's do a new round!
Here is the code again:
<?php
session_start();
require 'db.php';
// So empty($var) returns true/false based on if a variable exists (isset()) and it's value 
// Read: https://www.php.net/empty what is considered FALSE is it exists
// I'm guessing this will do:
if (empty($_SESSION['logged_in'])) {
    $_SESSION['message'] = "You must log in before viewing your profile page!";
    header("location: error.php");
    exit();
} else {
    // If you are sure $_SESSION['name'] exists if a user is logged in, this is fine.
    // Otherwise consider  $name = $_SESSION['name']?:'unknown';
    $name = $_SESSION['name'];
}
if (isset($_POST['stid'])) {
    // So you still allow a raw POST variable in your database query..
    // Don't do that or you might find someone messed around with your database.
    // https://www.w3schools.com/sql/sql_injection.asp
    // $pstid = $_POST['stid'];
    $pstid = mysqli_real_escape_string($link, $_POST['stid']);
    $tcid = mysqli_real_escape_string($link, $tcid); // Where does $tcid come from? Does it exist?
    // Check if the query is succesful and if there are results..
    // In the function documentation always peek at the Parameters and Return Values
    // https://php.net/manual/en/mysqli.query.php : Returns FALSE on failure.
    // https://www.php.net/manual/en/mysqli-result.fetch-array.php : Returns NULL if there are no more rows
    if($result = mysqli_query($link, "SELECT `id`, `tc_id`, `name`, `essay`, `image` FROM `students` WHERE tc_id = ".$tcid." AND id = ".$pstid.";")) {
        if($row = mysqli_fetch_array($result)) {
            $stname = $row['name'];
            $cessay = $row['essay'];
            $cimage = $row['image'];
        } else {
            $_SESSION['message'] = "No essay was found, please create one first.";
            header("location: error.php");
            exit();
        }
    } else {
        $_SESSION['message'] = "Something went wrong..";
        header("location: error.php");
        exit();
    }
    // What if $_POST['stid'] does not exist though?
    // You use it as hidden input for your form, so let's throw an error.
} else {
    $_SESSION['message'] = "Student id not found.";
    header("location: error.php");
    exit();
}
?>
<form method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label for="TextToUpload">Text:</label>
<!--
So, $cessay comes from your database; but how does it get entered? By students?
Imagine a students enters "</textarea><img src="https://i.imgur.com/BBcy6Wc.jpg">"
Right.. a cat picture will be shown.. Solution: Escape it.
https://www.w3schools.com/php/func_string_htmlspecialchars.asp
So:
--> 
        <textarea type="text" class="form-control" id="TextToUpload" rows="5" name="stessay"><?php echo $cessay; ?></textarea>
    </div>
    <p>Imagem do(a) aluno(a):</p>
    <?php
        if (isset($row['image'])) {
            // Same applies here, although cimage might be under your control, so less critical
            // Just make it a habbit to escape and you'll never have trouble :)
            $cimage = htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
            echo "<img src=\"resources/images/studentsImages/".$cimage."\">";
        }
        // So I've used it before in mysqli_real_escape_string, so it now could be something like '123' instead of 123
        // Since we now need to escape it not for SQL but HTML, just use the original value again:
        echo "<input type=\"hidden\" name=\"stid\" value=".htmlspecialchars($_POST['stid'], ENT_QUOTES, 'UTF-8').">";
    ?>
    <input type="file" name="fileToUpload" id="fileToUpload">
    <input formmethod="post" type="submit" value="savechanges" name="submit">
</form>
<?php
    if(isset($_POST["submit"])) {
        $essayOk = 0;
        $imageOk = 0;
        if(isset($_POST['stessay'])) {
            $essayOk = 1;
            $pessayc = mysqli_real_escape_string($link, $_POST['stessay']); 
            // Very good! :)
        }
        // Uploaded files should be in the $_FILES array! So don't use $_POST
        if (isset($_FILES['fileToUpload'])) {
            $target_dir = "resources/images/";
            $target_file = $target_dir."img".$pstid.".jpg";
            // $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION)); // Not needed, fails anyway      
            // But where is $finfo ? The code below will always fail without it..
            // Adding it back:
            $finfo = finfo_open(FILEINFO_MIME_TYPE);        
            if($mime = finfo_file($finfo, $_FILES["fileToUpload"]["tmp_name"])) {
                if($mime == 'image/jpg' || $mime == 'image/jpeg') {
                    if(getimagesize($_FILES["fileToUpload"]["tmp_name"]) === false) {
                        echo "File is corrupt";
                        $uploadOk = 0;
                    } else {
                        echo "File is an image - " . $mime . ".";
                        $uploadOk = 1;
                    }                           
                } else {
                    echo $mime . " is not supported.";
                    $uploadOk = 0;
                }
            } else {
                echo "Invalid file";
                $uploadOk = 0;
            }
            if ($_FILES["fileToUpload"]["size"] > 500000) {
                echo "The file is too big.";
                $imageOk = 0;
            }
            if ($imageOk == 0) {
                echo "Erros sending image, try again later.";
            }
        }
        if ($essayOk == 1) {
            $query = "UPDATE students SET essay = '".$pessayc."' WHERE id = '".$pstid."';";
            if (mysqli_query($link, $query)) {
                echo "<p>Essay updated!</p>";
            } else {
                printf("Errormessage: %s\n", mysqli_error($link));
                echo "<p>There was an error updating the ssay, try again later.</p>";
            }
        }
        if ($imageOk == 1) {
            if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
                echo "The file ".basename($_FILES["fileToUpload"]["name"])." was updated.";
            } else {
                echo "Your image was not updated, try again later.";
            }
        }
    }    
?>
As for your final question, check if it exists in the $_FILES array, but also I believe the filename should still be in the $_POST array. Not sure though. Add this on top of your code to see what variables are found:
print_r($_POST);
print_r($_FILES);
It will give you a nice list of POST fields and uploaded files.