@extends('layouts.app') @section('css') @endsection @section('content') @php // The sentiment vocabulary is admin-managed, so the pill's label and // colour are looked up by the word stored on the mail rather than // matched against names hardcoded here. Keyed by code, which is exactly // what messages.is_spam_value holds. $sentimentByCode = collect($counts['sentiment_defs'] ?? [])->keyBy('code'); // Next actions are looked up by the word stored on the mail, which is // exactly what messages.next_action holds. Aliases are folded into the // same map so a mail saved under an action's older name still resolves. // Follow-up tasks for the mails on this page, keyed by messages.id. // Defaulted rather than required: the list has to render on an install // that has not run the follow-up migration. $followUpsByMessage = $followUps ?? []; // Which cell holds what, by column number. The cards hide the column // headers, so the sort control has to name DataTables columns by index — // and the index moves, because the listing carries different columns for // different filters. Worked out here once, in the same order the cells // are written below. $colIndex = ['num' => 0, 'sender' => 1, 'subject' => 2, 'date' => 3, 'action' => 4, 'lead' => 5, 'assigned' => 6]; $nextCol = 7; if ($type == 'Reply pending mail') { $colIndex['waiting'] = $nextCol++; } if ($type == 'Replied Mail') { $colIndex['replied'] = $nextCol++; } if ($type == 'All Mail') { $colIndex['sentiment'] = $nextCol++; $colIndex['category'] = $nextCol++; $colIndex['flags'] = $nextCol++; } $colIndex['actions'] = $nextCol; $nextActionDefs = $counts['next_action_defs'] ?? []; $nextActionByCode = []; foreach ($nextActionDefs as $def) { $nextActionByCode[strtolower($def['code'])] = $def; foreach ($def['aliases'] ?? [] as $alias) { $nextActionByCode[$alias] = $def; } } // Lead types and categories resolve the same way, by the word stored on // the mail. Aliases are folded in so a mail classified under an older // name — or, for categories, under the bare `sales` / `amc` / `external` // the classifier used before the vocabularies were unified — still shows // a pill instead of a dash. $leadByCode = []; foreach ($counts['lead_defs'] ?? [] as $def) { $leadByCode[strtolower($def['code'])] = $def; } foreach ($counts['lead_defs'] ?? [] as $def) { foreach ($def['aliases'] ?? [] as $alias) { $leadByCode[$alias] ??= $def; } } // The priority bands, hoisted out of the row loop: three fixed entries // that would otherwise be rebuilt for every mail on the page. $leadPriorityDefs = $counts['lead_priority_defs'] ?? \App\Models\LeadType::priorityDefinitions(); // Assignment: teams keyed by id, and the name of everyone who can own // mail in this account. Both hoisted out of the row loop — the column // names a team and a person on every row, and the person is not always // the team's head, so the head on the chip definitions is not enough. $assignedByDept = []; foreach ($counts['assignment_defs'] ?? [] as $def) { if (($def['kind'] ?? '') === 'department') { $assignedByDept[(int) $def['department_id']] = $def; } } $assigneeNames = \App\Models\Department::assigneeNamesFor(auth()->id()); // Codes in a pass of their own, then first alias claimant wins. Category // aliases are not exclusive — four of the standard categories claim // `sales` — so the two passes are what stop one category's alias // shadowing another category's own code, and they make this pill agree // with the count in the Group By menu and the slice on the dashboard, // which resolve a word the same way. $categoryByCode = []; foreach ($counts['category_defs'] ?? [] as $def) { $categoryByCode[strtolower($def['code'])] = $def; } foreach ($counts['category_defs'] ?? [] as $def) { foreach ($def['aliases'] ?? [] as $alias) { $categoryByCode[$alias] ??= $def; } } @endphp

{{ $type }} {{ count($mails) }}

{{-- The cards hide the column headers, so the sorting those headers offered is offered here instead. --}}
{{--
--}}
{{--

{{ $type }}


--}} {{--

Welcome to your inbox Mail Dashboard

--}} {{--
--}} {{--

Emails ({{ $type }})

--}} {{-- {{ dd($mails) }} --}} @if ($mails->isEmpty())

No mails to show

There is nothing in “{{ $type }}” right now.

@else {{-- Still a table, because DataTables drives the search, the sorting and the pager off these rows — but emi-mail-list lays each row out as a card, so a mail reflows down the page instead of overflowing across it. The header row is hidden; the cells are placed by class. --}} {{-- --}} {{-- Name and address share one column. The address had a column of its own, which cost the width that Actions needed. --}} {{-- --}} {{-- What to do about this mail, so the list can be worked through without opening every row. Sorted by urgency, not alphabetically — see the data-order below. --}} {{-- Whether this mail is a sales opportunity, and what kind. Sorted by the classifier's own score, so the strongest signals come first — see the data-order below. --}} {{-- Who owns this mail. Sorted so unowned mail comes first — see the data-order below — because the reason to sort this column is to find what nobody has picked up. --}} {{-- --}} @if ($type == 'Reply pending mail') @endif @if ($type == 'Replied Mail') {{-- --}} @endif @if ($type == 'Read Mail' || $type == 'Un-Read Mail' || $type == 'All Mail') @if ($type == 'All Mail') {{-- What kind of mail this is, from the configured category vocabulary. Sales and AMC in the Group By menu are the coarse rollups of these. --}} {{-- Read state, the promotion/spam label and action-required share one column. As three columns they were mostly empty pills, and between them they pushed Actions off-screen. --}} @endif {{-- --}} {{-- @elseif ($type == 'send-mail') --}} @endif @php // Mailbox owner — used to flag the mails they sent themselves. $currentEmail = isset($employee) ? $employee->email : Auth::user()->email ?? null; @endphp @foreach ($mails as $mail) @php $isOwnSentMail = $currentEmail !== null && strcasecmp(trim($mail->sender_email), trim($currentEmail)) === 0; @endphp @php // Create DateTime objects in Asia/Kolkata timezone // $sent = new DateTime($mail->sent_date, new DateTimeZone('Asia/Kolkata')); $sent = new DateTime($mail->sent_date); // Let it parse the actual timezone $sent->setTimezone(new DateTimeZone('Asia/Kolkata')); // $replied = new DateTime($mail->sent_date_s, new DateTimeZone('Asia/Kolkata')); $replied = new DateTime($mail->sent_date_s); // Let it parse the actual timezone $replied->setTimezone(new DateTimeZone('Asia/Kolkata')); @endphp {{-- Kept for DataTables' benefit: it is column 0 and the listing's own order. The cards do not draw it. --}} {{-- --}} @php // Every per-row link is scoped to the mailbox the mail actually // sits in, not the mailbox in the page URL. The two differ on the // assignment listings: those span mailboxes on purpose (mail // assigned to an employee is delivered to whichever mailbox it was // addressed to), so a row here can belong to a different user than // $employee. Linking with $employee->id sent Reply/Open/Summary // looking for the mail under an owner that does not have it, and // they found nothing. $mailboxId = $mail->user_id ?: ($employee->id ?? null); // The mail's own page — the subject links to it, so reading a // mail costs no button in the Actions column. $mailUrl = route('mails.show', [ 'emial_id' => $mail->email_id, 'is_sent' => '0', 'user_id' => $mailboxId, ]); @endphp @php // Plenty of mail arrives with no name at all, or with the // address as the name. Either way the address stands in for // the name and the second line is dropped, because printing // the same address twice tells the reader nothing. $senderEmail = trim((string) $mail->sender_email); $senderName = trim((string) $mail->sender_name); $displayName = $senderName !== '' ? $senderName : $senderEmail; $showAddress = strcasecmp($displayName, $senderEmail) !== 0; @endphp {{-- --}} {{-- Next Action --}} @php $actionWord = strtolower(trim((string) ($mail->next_action ?? ''))); $actionDef = $nextActionByCode[$actionWord] ?? null; // Anything other than "ai" was worked out from this // mail's other labels rather than recommended for it, // which the pill says so as not to overclaim. $actionInferred = strtolower(trim((string) ($mail->next_action_source ?? ''))) !== 'ai'; // Sort by urgency: the vocabulary order is the ranking, // and mail with no action at all sorts last. $actionRank = $actionDef ? $actionDef['rank'] : 999; @endphp {{-- Lead --}} @php $isLead = (int) ($mail->is_potential_lead ?? 0) === 1; $leadWord = strtolower(trim((string) ($mail->lead_type ?? ''))); $leadDef = $leadByCode[$leadWord] ?? null; // Anything other than "ai" was worked out from this mail's // other labels rather than detected in it, which the pill // says so as not to overclaim. $leadInferred = strtolower(trim((string) ($mail->lead_source ?? ''))) !== 'ai'; // Null is not zero: an inferred lead was never scored, and // showing it as 0 would read as "the model was sure it is // not one". $leadScore = $mail->lead_score ?? null; $leadScore = $leadScore === null || $leadScore === '' ? null : (int) $leadScore; // How urgently this one needs answering: its type's band, // dropped one step when the classifier only just cleared // that type's floor. Derived here rather than read off the // row, so an admin who re-bands a type sees this column // change on the next page load. $leadBand = \App\Models\LeadType::priorityFor($mail); $leadBandDef = $leadBand === null ? null : $leadPriorityDefs[$leadBand]; // Sort by band first and certainty within it, so the column // ranks by what to do next rather than by how sure the model // was. Mail that is not a lead sorts last whatever else it // carries. $leadOrder = $isLead ? \App\Models\LeadType::priorityOrderValue($leadBand, $leadScore) : 0; $leadTitle = $isLead ? trim( ($leadInferred ? 'Inferred from this mail’s existing labels — it has not been through the lead classifier. ' : '') . (trim((string) ($mail->lead_reason ?? '')) ?: ($leadDef['description'] ?? 'Looks like a sales opportunity.')), ) : 'Not a sales opportunity.'; @endphp {{-- Assigned --}} @php $assignedDeptId = (int) ($mail->assigned_department_id ?? 0); $assignedDeptDef = $assignedByDept[$assignedDeptId] ?? null; $assignedToId = (int) ($mail->assigned_user_id ?? 0); $assignedToName = $assigneeNames[$assignedToId] ?? null; // A person decided this one, and it will not be changed // back by the rule or by mail:assign-existing. Worth // saying: it is the difference between "the mapping sent // this here" and "somebody put this here". $assignedManually = strtolower(trim((string) ($mail->assignment_source ?? ''))) === \App\Models\Department::SOURCE_MANUAL; // Unowned sorts first: the reason to sort this column is // to find what nobody has picked up. $assignedOrder = $assignedDeptId > 0 ? 1 : 0; @endphp @if ($type == 'Reply pending mail') @php $now = new DateTime('now', new DateTimeZone('Asia/Kolkata')); $interval = $sent->diff($now); // Difference formatting $diffParts = []; if ($interval->y) { $diffParts[] = "{$interval->y} years"; } if ($interval->m) { $diffParts[] = "{$interval->m} months"; } if ($interval->d) { $diffParts[] = "{$interval->d} days"; } if ($interval->h) { $diffParts[] = "{$interval->h} hours"; } if ($interval->i) { $diffParts[] = "{$interval->i} minutes"; } if ($interval->s) { $diffParts[] = "{$interval->s} seconds"; } // Full text kept for the tooltip $diffText = implode(' ', $diffParts) . ' ago'; // Compact label — the two most significant units only. $units = [ ['y', 'yr'], ['m', 'mo'], ['d', 'd'], ['h', 'h'], ['i', 'm'], ]; $shortParts = []; foreach ($units as [$prop, $suffix]) { if ($interval->$prop && count($shortParts) < 2) { $shortParts[] = $interval->$prop . $suffix; } } $shortText = $shortParts ? implode(' ', $shortParts) . ' ago' : 'Just now'; // Severity: the longer a reply is outstanding, the louder the pill. $totalDays = (int) $sent->diff($now)->days; if ($totalDays >= 30) { $pill = 'emi-pill--red'; } elseif ($totalDays >= 7) { $pill = 'emi-pill--amber'; } elseif ($totalDays >= 1) { $pill = 'emi-pill--blue'; } else { $pill = 'emi-pill--green'; } @endphp @endif @if ($type == 'Replied Mail') {{-- On a card this sits among the pills, so it says what the date is rather than relying on a column header. --}} @php $interval = $sent->diff($replied); $diffParts = []; if ($interval->y) { $diffParts[] = "{$interval->y} years"; } if ($interval->m) { $diffParts[] = "{$interval->m} months"; } if ($interval->d) { $diffParts[] = "{$interval->d} days"; } if ($interval->h) { $diffParts[] = "{$interval->h} hours"; } if ($interval->i) { $diffParts[] = "{$interval->i} minutes"; } if ($interval->s) { $diffParts[] = "{$interval->s} seconds"; } $diffText = implode(' ', $diffParts); // Color logic if ($interval->y >= 1) { $color = 'text-secondary'; } elseif ($interval->m >= 1) { $color = 'text-warning'; } elseif ($interval->d >= 2) { $color = 'text-danger'; } elseif ($interval->d == 1) { $color = 'text-primary'; } else { $color = 'text-success'; } @endphp {{-- --}} @endif @if ($type == 'Read Mail' || $type == 'Un-Read Mail' || $type == 'All Mail') @if ($type == 'All Mail') {{-- Sentiment --}} {{-- Category --}} @php $categoryWord = strtolower(trim((string) ($mail->is_category_value ?? ''))); $categoryDef = $categoryByCode[$categoryWord] ?? null; @endphp {{-- Flags. Only what is true about the mail is drawn: an unread pill, a promotion/spam label, and whether the mail is waiting on somebody. A plain read mail shows a dash rather than three "No" pills. Sorted so the rows carrying the most flags come first. --}} @php $getting_spam = strpos($mail->label_ids, 'SPAM') !== false ? 1 : 0; $flagScore = ($mail->message_status == 1 ? 0 : 4) + ($mail->is_promotion == 1 || $getting_spam == 1 ? 2 : 0) + ($mail->action_required == 1 ? 1 : 0); @endphp @endif {{-- --}} {{-- @elseif ($type == 'send-mail') --}} @endif {{-- --}} @endforeach
#User IDSender SubjectAttachmentSent DateNext Action Lead & Priority Assigned is_spamWaiting SinceReplied DateResponse TimeSentimentCategoryFlagsTypes MailSpam StatusActions
{{ $loop->iteration }}{{ $mail->email_id }} {{ $displayName }} @if ($isOwnSentMail) Sent @endif @if ($showAddress) {{ $senderEmail }} @endif {{ $mail->subject === 'No Subject' ? '(No subject)' : $mail->subject }} {{ $mail->attachment_status == 1 ? 'Yes' : 'No' }} {{ $sent->format('d M Y') }} {{ $sent->format('h:i A') }} @if ($actionDef) Action{{ $actionDef['label'] }} @else Action— @endif @if ($isLead) @if ($leadBandDef) {{-- Urgency before kind, because it is the reason to look at the column at all. The tooltip says how the band was arrived at: a band that cannot be explained is a band nobody trusts twice. --}} @php $ownBand = $leadDef['priority'] ?? null; $wasDemoted = $ownBand !== null && $ownBand !== $leadBand; $bandWhy = $wasDemoted ? ($leadDef['label'] ?? 'This type') . ' is a ' . strtolower($leadPriorityDefs[$ownBand]['label']) . '-priority type, but ' . $leadScore . ' only just clears its floor of ' . ($leadDef['min_score'] ?? 0) . ', so it is shown one band lower.' : $leadBandDef['description']; @endphp Priority{{ $leadBandDef['label'] }} @endif Lead{{ $leadDef['label'] ?? 'Lead' }} @if ($leadScore !== null) {{-- The classifier's own certainty. Shown beside the type because "Pricing Request, 40" and "Pricing Request, 95" are not the same claim. --}} {{ $leadScore }} @endif @else Lead— @endif @if ($assignedDeptId > 0) @php $assignedTeamLabel = $assignedDeptDef['label'] ?? 'Deleted team'; $assignedWhy = $assignedManually ? 'Assigned by hand' . ($assignedToName ? ' to ' . $assignedToName : '') . ' — this overrides the routing rule and is not changed back automatically.' : 'Routed automatically to ' . $assignedTeamLabel . ($assignedToName ? ', whose head is ' . $assignedToName . '.' : '. No head is named for this team, so it belongs to the team rather than to a person.'); @endphp {{ $assignedManually ? 'Owner' : 'Team' }}{{ $assignedTeamLabel }} @if ($assignedToName) {{-- The person, beside the team. "Support" and "Support / Pankaj" are different situations: one is a queue, the other is somebody's desk. --}} {{ $assignedToName }} @endif @else Team— @endif Waiting{{ $shortText }} Replied {{ $replied->format('d M Y, h:i A') }} {{ $diffText }} @php $getting_spam = strpos($mail->label_ids, 'SPAM') !== false ? 1 : 0; if ($mail->is_promotion == 1 || $getting_spam == 1) { $sentimentText = 'Mass mail'; $sentimentPill = 'emi-pill--violet'; } else { $sentimentDef = $sentimentByCode[strtolower(trim((string) $mail->is_spam_value))] ?? null; // Mail classified under a label that has since been // renamed or deactivated still shows the word it was // given, in a neutral pill. $sentimentText = $sentimentDef['label'] ?? ucfirst((string) $mail->is_spam_value); $sentimentPill = $sentimentDef['pill'] ?? 'emi-pill--slate'; } @endphp Sentiment{{ $sentimentText !== '' ? $sentimentText : '—' }} {{-- {{ $mail->is_promotion == 1 ? ($getting_spam == 1 ? 'Spam & Promotion' : 'Promotion') : ($getting_spam == 1 ? 'Spam' : ucfirst($mail->is_spam_value)) }} --}} {{-- {{ $mail->is_promotion == 1 ? $getting_spam . 'Promotion ' : ucfirst($mail->is_spam_value) }} --}} @if ($categoryDef) Category{{ $categoryDef['label'] }} @elseif ($categoryWord !== '') {{-- Classified under a code since renamed, retired, or belonging to another account's vocabulary. The word it was given is still worth showing. --}} Category{{ ucfirst(str_replace('_', ' ', $categoryWord)) }} @else Category— @endif @if ($mail->message_status != 1) Unread @endif @if ($mail->is_promotion == 1) Promotion @elseif ($getting_spam == 1) Spam @endif @if ($mail->action_required == 1) {{-- "Awaiting reply", not "Action": the next-action pill on the same card is already labelled Action, and these two say different things. --}} Awaiting reply @endif {{-- Nothing to say about a read, unlabelled mail with nothing outstanding, so the card says nothing — a column needed a dash to fill, a card does not. --}} @php switch ($mail->is_spam) { case 0: $statusText = 'Negative'; break; case 1: $statusText = 'Positive'; break; case 2: $statusText = 'Reply Mail'; break; case 3: $statusText = 'Neutral'; break; case 4: $statusText = 'Promotion'; break; case 5: $statusText = 'Sales'; break; case 6: $statusText = 'AMC'; break; default: $statusText = 'Unknown'; break; } @endphp {{ $statusText }} ok{{ $mail->is_spam }}1 {{-- Actions. Five buttons in a table column sat off the right edge of the window; on a card they sit at the end of the mail they belong to. Summary and Reply stay out here, opening the mail moved onto the subject, and the rest are in the menu. --}} @php $followUp = $followUpsByMessage[$mail->id] ?? null; $followUpLate = $followUp && $followUp->isOverdue(); $followUpOpen = $followUp && $followUp->isOpen(); // Track is only meaningful once a reply has gone out on a // mail that was waiting for one. $trackUrl = $mail->sent_mail_id != -1 && $mail->action_required == 1 ? route('mails.show', [ 'emial_id' => $mail->sent_mail_id, 'is_sent' => '1', 'user_id' => $mailboxId, ]) : null; @endphp
{{-- A mail already carrying a follow-up says when it is due, red once it is late, and links to the queue. --}} @if ($followUpOpen) {{ $followUp->dueLabel() }} @endif {{-- Email Summary: the whole conversation this mail belongs to, in five sections. Opens in a modal so the list is not lost; the modal offers the full page. --}} Summary {{-- Reply is the one link that does NOT take $mailboxId. Its user_id means "reply as this account", not "read from this mailbox": a mail assigned to an employee is answered by that employee, from their own address, even though it was delivered elsewhere. replyMail() resolves the mail's real mailbox itself for the purpose of reading it back. --}} Reply
@endif
{{-- k0530 9-9-2025 --}} {{-- k0530 9-9-2025 --}}
@endsection @push('scripts') {{-- DataTables CSS is loaded in layouts/app.blade.php so css/emi-ui.css can override it --}} @endpush