Assuming your draft formula is correct the following might work:
var Text QTA = Part(UserListToNames([Quality Team]), 1, ";");
var Text QTB = Part(UserListToNames([Quality Team]), 2, ";");
var Text QTC = Part(UserListToNames([Quality Team]), 3, ";");
var Text QTD = Part(UserListToNames([Quality Team]), 4, ";");
If (not Contains([Quality Approval], $QTA & "] Approved"), $QTA) &
If (not Contains([Quality Approval], $QTB & "] Approved"), $QTB) &
If (not Contains([Quality Approval], $QTC & "] Approved"), $QTC) &
If (not Contains([Quality Approval], $QTD & "] Approved"), $QTD)
The formula basically concatenates together those users in the Quality Team that
don't appear in the Quality Approval log.
But if there is a possibility that in the Quality tame may initially approve the request but later withdraw their approval the above formula would incorrectly classify their actions a approved.
Script would provide a solution that covers all cases.
Another approach would be to create a child table and the appropriate summary fields and reports. But I assume you like the compact display of the user list and logging field. I would like to hear Mark's input on using the child table.