1
0
mirror of https://github.com/freescout-helpdesk/freescout.git synced 2025-01-31 20:11:38 +01:00

Conversation status change

This commit is contained in:
FreeScout 2018-07-25 10:25:00 -07:00
parent 4825d58d4a
commit 5a38ed1249
18 changed files with 396 additions and 198 deletions

View File

@ -3,6 +3,7 @@
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Folder;
class Conversation extends Model
{
@ -101,7 +102,7 @@ class Conversation extends Model
/**
* Automatically converted into Carbon dates.
*/
protected $dates = ['created_at', 'updated_at', 'last_reply_at'];
protected $dates = ['created_at', 'updated_at', 'last_reply_at', 'closed_at'];
/**
* Attributes which are not fillable using fill() method.
@ -165,6 +166,30 @@ class Conversation extends Model
return $this->belongsTo('App\Customer');
}
/**
* Get user who created the conversations.
*/
public function created_by_user()
{
return $this->belongsTo('App\User');
}
/**
* Get customer who created the conversations.
*/
public function created_by_customer()
{
return $this->belongsTo('App\Customer');
}
/**
* Get user who closed the conversations.
*/
public function closed_by_user()
{
return $this->belongsTo('App\User');
}
/**
* Set preview text.
*
@ -244,6 +269,25 @@ class Conversation extends Model
}
}
/**
* Set conersation status and all related fields.
*
* @param integer $status
*/
public function setStatus($status, $user = null)
{
$now = date('Y-m-d H:i:s');
$this->status = $status;
$this->updateFolder();
$this->user_updated_at = $now;
if ($user && $status == self::STATUS_CLOSED) {
$this->closed_by_user_id = $user->id;
$this->closed_at = $now;
}
}
/**
* Get next active conversation.
*
@ -299,4 +343,33 @@ class Conversation extends Model
return $query_prev->first();
}
/**
* Set folder according to the status, sate and user of the conversation.
*/
public function updateFolder()
{
if ($this->state == self::STATE_DRAFT) {
$folder_type = Folder::TYPE_DRAFTS;
} elseif ($this->state == self::STATE_DELETED) {
$folder_type = Folder::TYPE_DELETED;
} elseif ($this->status == self::STATUS_SPAM) {
$folder_type = Folder::TYPE_SPAM;
} elseif ($this->status == self::STATUS_CLOSED) {
$folder_type = Folder::TYPE_CLOSED;
} elseif ($this->user_id) {
$folder_type = Folder::TYPE_ASSIGNED;
} else {
$folder_type = Folder::TYPE_UNASSIGNED;
}
// Find folder
$folder = $this->mailbox->folders()
->where('type', $folder_type)
->first();
if ($folder) {
$this->folder_id = $folder->id;
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use App\Conversation;
class ConversationStatusChanged
{
public $conversation;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Conversation $conversation)
{
$this->conversation = $conversation;
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class Event
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Conversation;
use Illuminate\Http\Request;
use App\Events\ConversationStatusChanged;
class ConversationsController extends Controller
{
@ -30,7 +31,7 @@ class ConversationsController extends Controller
'conversation' => $conversation,
'mailbox' => $conversation->mailbox,
'customer' => $conversation->customer,
'threads' => $conversation->threads,
'threads' => $conversation->threads()->orderBy('created_at', 'desc')->get(),
'folder' => $conversation->folder,
'folders' => $conversation->mailbox->getAssesibleFolders(),
]);
@ -43,17 +44,20 @@ class ConversationsController extends Controller
'msg' => '',
);
$user = auth()->user();
switch ($request->action) {
case 'change_status':
$conversation = Conversation::find($request->conversation_id);
$new_status = (int)$request->status;
if (!$conversation) {
$response['msg'] = 'Conversation not found';
}
if (!$response['msg'] && $conversation->status == $new_status) {
$response['msg'] = 'Status already set';
}
if (!$response['msg'] && !auth()->user()->can('update', $conversation)) {
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = 'Not enough permissions';
}
if (!$response['msg'] && !in_array((int)$request->status, array_keys(Conversation::$statuses))) {
@ -63,9 +67,11 @@ class ConversationsController extends Controller
// Next conversation has to be determined before updating status for current one
$next_conversation = $conversation->getNearby();
$conversation->status = $new_status;
$conversation->user_updated_at = date('Y-m-d H:i:s');
$conversation->setStatus($new_status, $user);
$conversation->save();
event(new ConversationStatusChanged($conversation));
$response['status'] = 'success';
// Flash

View File

@ -0,0 +1,42 @@
<?php
namespace App\Listeners;
use App\Events\ConversationStatusChanged;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Thread;
class CreateThreadStatusChanged
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ConversationStatusChanged $event
* @return void
*/
public function handle(ConversationStatusChanged $event)
{
$thread = new Thread();
$thread->conversation_id = $event->conversation->id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->action_type = Thread::ACTION_TYPE_STATUS_CHANGED;
$thread->source_via = Thread::PERSON_USER;
// todo: this need to be changed for API
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $event->conversation->customer_id;
$thread->created_by_user_id = auth()->user()->id;
$thread->save();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Listeners;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateMailboxCounters
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle($event)
{
$event->conversation->mailbox->updateFoldersCounters();
}
}

View File

@ -16,6 +16,7 @@ class MailboxObserver
*/
public function created(Mailbox $mailbox)
{
// Create folders
foreach (Folder::$public_types as $type) {
$folder = new Folder();
$folder->mailbox_id = $mailbox->id;

View File

@ -36,6 +36,11 @@ class EventServiceProvider extends ServiceProvider
'Illuminate\Auth\Events\PasswordReset' => [
'App\Listeners\LogPasswordReset',
],
'App\Events\ConversationStatusChanged' => [
'App\Listeners\CreateThreadStatusChanged',
'App\Listeners\UpdateMailboxCounters',
],
];
/**

View File

@ -7,18 +7,18 @@ use Illuminate\Database\Eloquent\Model;
class Thread extends Model
{
/**
* By whom action performed (source_via).
* By whom action performed (source_via)
*/
const PERSON_CUSTOMER = 1;
const PERSON_USER = 2;
public static $persons = [
public static $persons = array(
self::PERSON_CUSTOMER => 'customer',
self::PERSON_USER => 'user',
];
self::PERSON_USER => 'user',
);
/**
* Thread types.
* Thread types
*/
// Email from customer
const TYPE_CUSTOMER = 1;
@ -35,23 +35,23 @@ class Thread extends Model
public static $types = [
// Thread by customer
self::TYPE_CUSTOMER => 'customer',
self::TYPE_CUSTOMER => 'customer',
// Thread by user
self::TYPE_MESSAGE => 'message',
self::TYPE_NOTE => 'note',
self::TYPE_MESSAGE => 'message',
self::TYPE_NOTE => 'note',
// lineitem represents a change of state on the conversation. This could include, but not limited to, the conversation was assigned, the status changed, the conversation was moved from one mailbox to another, etc. A line item wont have a body, to/cc/bcc lists, or attachments.
self::TYPE_LINEITEM => 'lineitem',
self::TYPE_PHONE => 'phone',
self::TYPE_LINEITEM => 'lineitem',
self::TYPE_PHONE => 'phone',
// When a conversation is forwarded, a new conversation is created to represent the forwarded conversation.
// forwardparent is the type set on the thread of the original conversation that initiated the forward event.
self::TYPE_FORWARDPARENT => 'forwardparent',
self::TYPE_FORWARDPARENT => 'forwardparent',
// forwardchild is the type set on the first thread of the new forwarded conversation.
self::TYPE_FORWARDCHILD => 'forwardchild',
self::TYPE_CHAT => 'chat',
self::TYPE_FORWARDCHILD => 'forwardchild',
self::TYPE_CHAT => 'chat',
];
/**
* Statuses.
* Statuses
*/
const STATUS_ACTIVE = 1;
const STATUS_CLOSED = 2;
@ -59,79 +59,85 @@ class Thread extends Model
const STATUS_PENDING = 4;
const STATUS_SPAM = 5;
public static $statuses = [
self::STATUS_ACTIVE => 'active',
self::STATUS_CLOSED => 'closed',
self::STATUS_NOCHANGE => 'nochange',
self::STATUS_PENDING => 'pending',
self::STATUS_SPAM => 'spam',
];
public static $statuses = array(
self::STATUS_ACTIVE => 'active',
self::STATUS_CLOSED => 'closed',
self::STATUS_NOCHANGE => 'nochange',
self::STATUS_PENDING => 'pending',
self::STATUS_SPAM => 'spam',
);
/**
* States.
* States
*/
const STATE_DRAFT = 1;
const STATE_PUBLISHED = 2;
const STATE_HIDDEN = 3;
const STATE_REVIEW = 4;
public static $states = [
self::STATE_DRAFT => 'draft',
self::STATE_PUBLISHED => 'published',
self::STATE_HIDDEN => 'hidden',
self::STATE_REVIEW => 'review',
];
public static $states = array(
self::STATE_DRAFT => 'draft',
self::STATE_PUBLISHED => 'published',
self::STATE_HIDDEN => 'hidden',
self::STATE_REVIEW => 'review',
);
/**
* Action associated with the line item.
* Action associated with the line item
*/
// Conversation's status changed
const ACTION_TYPE_STATUS_CHANGED = 1;
// Conversation's assignee changed
const ACTION_TYPE_USER_CHANGED = 2;
// The conversation was moved from another mailbox
const ACTION_TYPE_MOVED_FROM_MAILBOX = 1;
const ACTION_TYPE_MOVED_FROM_MAILBOX = 3;
// Another conversation was merged with this conversation
const ACTION_TYPE_MERGED = 2;
const ACTION_TYPE_MERGED = 4;
// The conversation was imported (no email notifications were sent)
const ACTION_TYPE_IMPORTED = 3;
const ACTION_TYPE_IMPORTED = 5;
// A workflow was run on this conversation (either automatic or manual)
const ACTION_TYPE_WORKFLOW_MANUAL = 4;
const ACTION_TYPE_WORKFLOW_AUTO = 5;
const ACTION_TYPE_WORKFLOW_MANUAL = 6;
const ACTION_TYPE_WORKFLOW_AUTO = 7;
// The ticket was imported from an external Service
const ACTION_TYPE_IMPORTED_EXTERNAL = 6;
const ACTION_TYPE_IMPORTED_EXTERNAL = 8;
// The customer associated with the ticket was changed
const ACTION_TYPE_CHANGED_TICKET_CUSTOMER = 7;
const ACTION_TYPE_CHANGED_TICKET_CUSTOMER = 9;
// The ticket was deleted
const ACTION_TYPE_DELETED_TICKET = 8;
const ACTION_TYPE_DELETED_TICKET = 10;
// The ticket was restored
const ACTION_TYPE_RESTORE_TICKET = 9;
const ACTION_TYPE_RESTORE_TICKET = 11;
// Describes an optional action associated with the line item
// todo: values need to be checked via HelpScout API
public static $action_types = [
self::ACTION_TYPE_MOVED_FROM_MAILBOX => 'moved-from-mailbox',
self::ACTION_TYPE_MERGED => 'merged',
self::ACTION_TYPE_IMPORTED => 'imported',
self::ACTION_TYPE_WORKFLOW_MANUAL => 'manual-workflow',
self::ACTION_TYPE_WORKFLOW_AUTO => 'automatic-workflow',
self::ACTION_TYPE_IMPORTED_EXTERNAL => 'imported-external',
self::ACTION_TYPE_CHANGED_TICKET_CUSTOMER => 'changed-ticket-customer',
self::ACTION_TYPE_DELETED_TICKET => 'deleted-ticket',
self::ACTION_TYPE_RESTORE_TICKET => 'restore-ticket',
self::ACTION_TYPE_STATUS_CHANGED => 'changed-ticket-status',
self::ACTION_TYPE_USER_CHANGED => 'changed-ticket-assignee',
self::ACTION_TYPE_MOVED_FROM_MAILBOX => 'moved-from-mailbox',
self::ACTION_TYPE_MERGED => 'merged',
self::ACTION_TYPE_IMPORTED => 'imported',
self::ACTION_TYPE_WORKFLOW_MANUAL => 'manual-workflow',
self::ACTION_TYPE_WORKFLOW_AUTO => 'automatic-workflow',
self::ACTION_TYPE_IMPORTED_EXTERNAL => 'imported-external',
self::ACTION_TYPE_CHANGED_TICKET_CUSTOMER => 'changed-ticket-customer',
self::ACTION_TYPE_DELETED_TICKET => 'deleted-ticket',
self::ACTION_TYPE_RESTORE_TICKET => 'restore-ticket',
];
/**
* Source types (equal to thread source types).
/**
* Source types (equal to thread source types)
*/
const SOURCE_TYPE_EMAIL = 1;
const SOURCE_TYPE_WEB = 2;
const SOURCE_TYPE_API = 3;
public static $source_types = [
self::SOURCE_TYPE_EMAIL => 'email',
self::SOURCE_TYPE_WEB => 'web',
self::SOURCE_TYPE_API => 'api',
self::SOURCE_TYPE_EMAIL => 'email',
self::SOURCE_TYPE_WEB => 'web',
self::SOURCE_TYPE_API => 'api',
];
/**
* Status of the email sent to the customer or user, to whom the thread is assigned.
/**
* Status of the email sent to the customer or user, to whom the thread is assigned
*/
const SEND_STATUS_TOSEND = 1;
const SEND_STATUS_SENT = 2;
@ -139,7 +145,7 @@ class Thread extends Model
const SEND_STATUS_DELIVERY_ERROR = 4;
/**
* The user assigned to this thread (assignedTo).
* The user assigned to this thread (assignedTo)
*/
public function user()
{
@ -147,7 +153,7 @@ class Thread extends Model
}
/**
* Get the thread customer.
* Get the thread customer
*/
public function customer()
{
@ -155,7 +161,7 @@ class Thread extends Model
}
/**
* Get conversation.
* Get conversation
*/
public function conversation()
{
@ -163,8 +169,23 @@ class Thread extends Model
}
/**
* Get sanitized body HTML.
*
* Get user who created the thread.
*/
public function created_by_user()
{
return $this->belongsTo('App\User');
}
/**
* Get customer who created the thread.
*/
public function created_by_customer()
{
return $this->belongsTo('App\Customer');
}
/**
* Get sanitized body HTML
* @return string
*/
public function getCleanBody()
@ -174,7 +195,7 @@ class Thread extends Model
/**
* Get thread recipients.
*
*
* @return array
*/
public function getTos()
@ -188,7 +209,7 @@ class Thread extends Model
/**
* Get thread CC recipients.
*
*
* @return array
*/
public function getCcs()
@ -202,7 +223,7 @@ class Thread extends Model
/**
* Get thread BCC recipients.
*
*
* @return array
*/
public function getBccs()
@ -215,33 +236,42 @@ class Thread extends Model
}
/**
* Get status name. Made as a function to allow status names translation.
*
* @param int $status
*
* Get thread's status name.
*
* @return string
*/
public static function getStatusName($status)
public function getStatusName()
{
return self::statusCodeToName($this->status);
}
/**
* Get status name. Made as a function to allow status names translation.
*
* @param integer $status
* @return string
*/
public static function statusCodeToName($status)
{
switch ($status) {
case self::STATUS_ACTIVE:
return __('Active');
return __("Active");
break;
case self::STATUS_PENDING:
return __('Pending');
return __("Pending");
break;
case self::STATUS_CLOSED:
return __('Closed');
return __("Closed");
break;
case self::STATUS_SPAM:
return __('Spam');
return __("Spam");
break;
case self::STATUS_NOCHANGE:
return __('Not changed');
return __("Not changed");
break;
default:

View File

@ -3,13 +3,13 @@
* User model class.
* Class also responsible for dates conversion and representation.
*/
namespace App;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Hash;
use App\Mailbox;
use Carbon\Carbon;
class User extends Authenticatable
{
@ -19,41 +19,41 @@ class User extends Authenticatable
// const UPDATED_AT = 'modified_at';
/**
* Roles.
* Roles
*/
const ROLE_USER = 1;
const ROLE_ADMIN = 2;
public static $roles = [
public static $roles = array(
self::ROLE_ADMIN => 'admin',
self::ROLE_USER => 'user',
];
self::ROLE_USER => 'user'
);
/**
* Types.
* Types
*/
const TYPE_USER = 1;
const TYPE_TEAM = 2;
/**
* Invite states.
* Invite states
*/
const INVITE_STATE_ACTIVATED = 0;
const INVITE_STATE_NOT_INVITED = 1;
const INVITE_STATE_SENT = 2;
/**
* Time formats.
* Time formats
*/
const TIME_FORMAT_12 = 1;
const TIME_FORMAT_24 = 2;
/**
* The attributes that are not mass assignable.
*
* @var array
*/
protected $guarded = ['role'];
protected $guarded = ['role'];
/**
* The attributes that should be hidden for arrays, excluded from the model's JSON form.
@ -65,14 +65,13 @@ class User extends Authenticatable
];
/**
* Attributes fillable using fill() method.
*
* Attributes fillable using fill() method
* @var [type]
*/
protected $fillable = ['role', 'first_name', 'last_name', 'email', 'password', 'role', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts'];
protected $fillable = ['role', 'first_name', 'last_name', 'email', 'password', 'role', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts'];
/**
* Get mailboxes to which usre has access.
* Get mailboxes to which usre has access
*/
public function mailboxes()
{
@ -80,7 +79,7 @@ class User extends Authenticatable
}
/**
* Get conversations assigned to user.
* Get conversations assigned to user
*/
public function conversations()
{
@ -88,7 +87,7 @@ class User extends Authenticatable
}
/**
* User's folders.
* User's folders
*/
public function folders()
{
@ -96,8 +95,8 @@ class User extends Authenticatable
}
/**
* Get user role.
*
* Get user role
*
* @return string
*/
public function getRoleName($ucfirst = false)
@ -106,32 +105,30 @@ class User extends Authenticatable
if ($ucfirst) {
$role_name = ucfirst($role_name);
}
return $role_name;
}
/**
* Check if user is admin.
*
* @return bool
* Check if user is admin
*
* @return boolean
*/
public function isAdmin()
{
return $this->role == self::ROLE_ADMIN;
return ($this->role == self::ROLE_ADMIN);
}
/**
* Get user full name.
*
* Get user full name
* @return string
*/
public function getFullName()
{
return $this->first_name.' '.$this->last_name;
return $this->first_name . ' ' . $this->last_name;
}
/**
* Get mailboxes to which user has access.
* Get mailboxes to which user has access
*/
public function mailboxesCanView()
{
@ -143,22 +140,18 @@ class User extends Authenticatable
}
/**
* Generate random password for the user.
*
* @param int $length
*
* Generate random password for the user
* @param integer $length
* @return string
*/
public function generatePassword($length = 8)
{
$this->password = Hash::make(str_random($length));
return $this->password;
}
/**
* Get URL for editing user.
*
* Get URL for editing user
* @return string
*/
public function urlEdit()
@ -168,9 +161,9 @@ class User extends Authenticatable
/**
* Create personal folders for user mailboxes.
*
* @param int $mailbox_id
* @param mixed $users
*
* @param integer $mailbox_id
* @param mixed $users
*/
public function syncPersonalFolders($mailboxes)
{
@ -192,7 +185,7 @@ class User extends Authenticatable
continue;
}
foreach (Folder::$personal_types as $type) {
$folder = new Folder();
$folder = new Folder;
$folder->mailbox_id = $mailbox_id;
$folder->user_id = $this->id;
$folder->type = $type;
@ -203,11 +196,10 @@ class User extends Authenticatable
/**
* Format date according to user's timezone and time format.
*
* @param Carbon $date
* @param string $format
*
* @return string
*
* @param Carbon $date
* @param string $format
* @return string
*/
public static function dateFormat($date, $format)
{
@ -215,16 +207,16 @@ class User extends Authenticatable
if ($user) {
if ($user->time_format == self::TIME_FORMAT_12) {
$format = strtr($format, [
'H' => 'h',
'G' => 'g',
':i' => ':ia',
'H' => 'h',
'G' => 'g',
':i' => ':ia',
':ia:s' => ':i:sa',
]);
} else {
$format = strtr($format, [
'h' => 'H',
'g' => 'G',
':ia' => ':i',
'h' => 'H',
'g' => 'G',
':ia' => ':i',
':i:sa' => ':i:s',
]);
}
@ -238,31 +230,33 @@ class User extends Authenticatable
/**
* Convert date into human readable format.
*
* @param Carbon $date
*
*
* @param Carbon $date
* @return string
*/
public static function dateDiffForHumans($date)
{
if (!$date) {
return '';
}
$user = auth()->user();
if ($user) {
$date->setTimezone($user->timezone);
}
if ($date->diffInSeconds(Carbon::now()) <= 60) {
return __('Just now');
return __("Just now");
} elseif ($date->diffInDays(Carbon::now()) > 7) {
// Exact date
if (Carbon::now()->year == $date->year) {
return self::dateFormat($date, 'M j');
return User::dateFormat($date, "M j");
} else {
return self::dateFormat($date, 'M j, Y');
return User::dateFormat($date, "M j, Y");
}
} else {
$diff_text = $date->diffForHumans();
$diff_text = preg_replace('/minutes?/', 'min', $diff_text);
$diff_text = preg_replace("/minutes?/", 'min', $diff_text);
return $diff_text;
}
}

View File

@ -4,11 +4,11 @@ use App\Conversation;
use Faker\Generator as Faker;
$factory->define(Conversation::class, function (Faker $faker, $params) {
if (!empty($params['created_by'])) {
$created_by = $params['created_by'];
if (!empty($params['created_by_user_id'])) {
$created_by_user_id = $params['created_by_user_id'];
} else {
// Pick random user
$created_by = App\User::inRandomOrder()->first()->id;
$created_by_user_id = App\User::inRandomOrder()->first()->id;
}
$folder_id = null;
if (!empty($params['folder_id'])) {
@ -26,14 +26,14 @@ $factory->define(Conversation::class, function (Faker $faker, $params) {
return [
'type' => $faker->randomElement([Conversation::TYPE_EMAIL, Conversation::TYPE_PHONE]),
'folder_id' => $folder_id,
'state' => $faker->randomElement(array_keys(Conversation::$states)),
'state' => Conversation::STATE_PUBLISHED, // $faker->randomElement(array_keys(Conversation::$states)),
'subject' => $faker->sentence(7),
// todo: cc and bcc must be equal to first (or last?) thread of conversation
'cc' => json_encode([$faker->unique()->safeEmail]),
'bcc' => json_encode([$faker->unique()->safeEmail]),
'preview' => $faker->text(Conversation::PREVIEW_MAXLENGTH),
'imported' => true,
'created_by' => $created_by,
'created_by_user_id' => $created_by_user_id,
'source_via' => Conversation::PERSON_CUSTOMER,
'source_type' => Conversation::SOURCE_TYPE_EMAIL,
];

View File

@ -34,6 +34,6 @@ $factory->define(Thread::class, function (Faker $faker, $params) {
'bcc' => json_encode([$faker->unique()->safeEmail]),
'source_via' => Thread::PERSON_CUSTOMER,
'source_type' => Thread::SOURCE_TYPE_EMAIL,
'created_by' => $customer_id,
'created_by_customer_id' => $customer_id,
];
});

View File

@ -42,13 +42,14 @@ class CreateConversationsTable extends Migration
// Originating source of the conversation - user or customer
// ID of the customer or user who created the conversation
// createdBy in the API
$table->integer('created_by');
$table->integer('created_by_user_id')->nullable();
$table->integer('created_by_customer_id')->nullable();
// source.via - Originating source of the conversation - user or customer
$table->unsignedTinyInteger('source_via');
// source.type - Originating type of the conversation (email, web, API etc)
$table->unsignedTinyInteger('source_type');
// closedBy - ID of the user who closed the conversation
$table->integer('closed_by_user')->nullable();
$table->integer('closed_by_user_id')->nullable();
// UTC time when the conversation was closed
$table->timestamp('closed_at')->nullable();
// UTC time when the last user update occurred

View File

@ -1,9 +1,9 @@
<?php
use App\Thread;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use App\Thread;
class CreateThreadsTable extends Migration
{
@ -18,6 +18,7 @@ class CreateThreadsTable extends Migration
$table->increments('id');
$table->integer('conversation_id');
// assignedTo - The user assigned to this thread
// used to display user who was assigned to the thread in the conversation
$table->integer('user_id')->nullable();
$table->unsignedTinyInteger('type');
$table->unsignedTinyInteger('status')->default(Thread::STATUS_ACTIVE);
@ -25,7 +26,8 @@ class CreateThreadsTable extends Migration
// Describes an optional action associated with the line item
$table->unsignedTinyInteger('action_type')->nullable();
$table->string('action_text', 255)->nullable();
$table->text('body', 65535);
// lineitems do not have body
$table->text('body', 65535)->nullable();
// Original body after thread text is changed
$table->text('body_original', 65535)->nullable();
$table->text('to')->nullable(); // JSON
@ -38,9 +40,10 @@ class CreateThreadsTable extends Migration
// customer - If thread type is message, this is the customer associated with the thread.
// If thread type is customer, this is the the customer who initiated the thread.
$table->integer('customer_id');
// Who created this thread. The type property will specify whether it was created by a user or a customer.
// Who created this thread. The source_via property will specify whether it was created by a user or a customer.
// See source_via
$table->integer('created_by');
$table->integer('created_by_user_id')->nullable();
$table->integer('created_by_customer_id')->nullable();
// ID of Saved reply that was used to create this Thread (savedReplyId)
$table->integer('saved_reply_id')->nullable();
// Status of the email sent to customer or user, to whom the thread is assigned

View File

@ -12,7 +12,10 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
//$this->call(MailboxesTableSeeder::class);
// Create users
factory(App\User::class, 3)->create();
// Create mailboxes, conversations, etc
factory(App\Mailbox::class, 3)->create()->each(function ($m) {
$user = factory(App\User::class)->create();
$m->users()->save($user);
@ -22,7 +25,7 @@ class DatabaseSeeder extends Seeder
$customer->emails()->save(factory(App\Email::class)->make());
$conversation = factory(App\Conversation::class)->create(['created_by' => $user->id, 'mailbox_id' => $m->id, 'customer_id' => $customer->id, 'user_id' => $user->id, 'status' => array_rand([Conversation::STATUS_ACTIVE => 1, Conversation::STATUS_PENDING => 1])]);
$conversation = factory(App\Conversation::class)->create(['created_by_user_id' => $user->id, 'mailbox_id' => $m->id, 'customer_id' => $customer->id, 'user_id' => $user->id, 'status' => array_rand([Conversation::STATUS_ACTIVE => 1, Conversation::STATUS_PENDING => 1])]);
$thread = factory(App\Thread::class)->make(['customer_id' => $customer->id, 'to' => $customer->getMainEmail(), 'conversation_id' => $conversation->id]);
$conversation->threads()->save($thread);

20
public/css/style.css vendored
View File

@ -632,7 +632,7 @@ a h4 {
text-decoration: none;
}
.customer-data {
padding: 0px 20px 0 18px;
padding: 0px 26px 0 18px;
margin-top: 15px;
margin-bottom: 15px;
}
@ -983,6 +983,7 @@ table.table-conversations td.conv-attachment {
background-color: #f1f3f5;
width: 280px;
height: 100%;
z-index: 9;
}
#conv-toolbar {
border-bottom: 1px solid #e3e8eb;
@ -1143,6 +1144,10 @@ table.table-conversations td.conv-attachment {
}
.thread {
position: relative;
border-bottom: 1px solid #e3e8eb;
}
.thread:last-child {
border-bottom: 0;
}
.thread-header {
overflow: hidden;
@ -1201,6 +1206,19 @@ table.table-conversations td.conv-attachment {
right: 13px;
color: #a5b2bd;
}
.thread-lineitem {
background: #f9fafa;
overflow: auto;
}
.thread-lineitem .thread-message {
padding-top: 12px;
padding-bottom: 9px;
color: #a5b2bd;
font-size: 12px;
}
.thread-lineitem .thread-header {
min-height: auto;
}
@media (max-width:1100px) {
#conv-layout-header,
#conv-layout-main,

View File

@ -0,0 +1,5 @@
@if ($thread->created_by_user->id == Auth::user()->id)
{{ __("you") }}
@else
{{ $thread->created_by_user->getFullName() }}
@endif

View File

@ -128,9 +128,14 @@
<div class="thread-message">
<div class="thread-header">
<div class="thread-title">
@include('conversations/thread_by')
@if ($thread->action_type == App\Thread::ACTION_TYPE_STATUS_CHANGED)
{{ __("marked as") }} {{ $thread->getStatusName() }}
@elseif ($thread->action_type == App\Thread::ACTION_TYPE_USER_CHANGED)
@endif
</div>
<div class="thread-info">
<span class="thread-date"></span>
<span class="thread-date">{{ App\User::dateDiffForHumans($thread->created_at) }}</span>
</div>
</div>
</div>
@ -148,11 +153,7 @@
@if ($thread->type == App\Thread::TYPE_CUSTOMER)
{{ $thread->customer->getFullName() }}
@else
@if ($thread->user->id == Auth::user()->id)
{{ __("you") }}
@else
{{ $thread->user->getFullName() }}
@endif
@include('conversations/thread_by')
@endif
</strong>
@if ($loop->index == 0)
@ -208,7 +209,7 @@
@endif
@endif
@if (!empty($show_status))
{{ App\Thread::getStatusName($thread->status) }}
{{ $thread->getStatusName() }}
@endif
</span>
@endif