diff --git a/.gitignore b/.gitignore index 7cc6a79c6d20..ae60d95a93d8 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /app/config/development /app/storage/ /public/logo +/public/build /bootstrap/compiled.php /vendor composer.phar diff --git a/app/commands/SendRecurringInvoices.php b/app/commands/SendRecurringInvoices.php index b5e3fb874359..4829555bf22d 100755 --- a/app/commands/SendRecurringInvoices.php +++ b/app/commands/SendRecurringInvoices.php @@ -24,7 +24,7 @@ class SendRecurringInvoices extends Command { $today = new DateTime(); - $invoices = Invoice::with('account', 'invoice_items')->whereRaw('frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get(); + $invoices = Invoice::with('account', 'invoice_items')->whereRaw('is_recurring is true AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get(); $this->info(count($invoices) . ' recurring invoice(s) found'); foreach ($invoices as $recurInvoice) @@ -59,7 +59,7 @@ class SendRecurringInvoices extends Command { $recurInvoice->last_sent_date = new DateTime(); $recurInvoice->save(); - $this->mailer->sendInvoice($invoice, $invoice->client->contacts()->first()); + $this->mailer->sendInvoice($invoice); } $this->info('Done'); diff --git a/app/controllers/InvoiceController.php b/app/controllers/InvoiceController.php index 90dd83b4de79..24003038afe5 100755 --- a/app/controllers/InvoiceController.php +++ b/app/controllers/InvoiceController.php @@ -21,7 +21,7 @@ class InvoiceController extends \BaseController { 'columns'=>['checkbox', 'Invoice Number', 'Client', 'Invoice Date', 'Invoice Total', 'Balance Due', 'Due Date', 'Status', 'Action'] ]; - if (Invoice::scope()->where('frequency_id', '>', '0')->count() > 0) + if (Invoice::scope()->where('is_recurring', '=', true)->count() > 0) { $data['secEntityType'] = ENTITY_RECURRING_INVOICE; $data['secColumns'] = ['checkbox', 'Frequency', 'Client', 'Start Date', 'End Date', 'Invoice Total', 'Action']; @@ -37,7 +37,7 @@ class InvoiceController extends \BaseController { ->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id') ->where('invoices.account_id', '=', Auth::user()->account_id) ->where('invoices.deleted_at', '=', null) - ->where('invoices.frequency_id', '=', 0) + ->where('invoices.is_recurring', '=', false) ->select('clients.public_id as client_public_id', 'invoice_number', 'clients.name as client_name', 'invoices.public_id', 'total', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name'); if ($clientPublicId) { @@ -97,7 +97,7 @@ class InvoiceController extends \BaseController { ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->where('invoices.account_id', '=', Auth::user()->account_id) ->where('invoices.deleted_at', '=', null) - ->where('invoices.frequency_id', '>', 0) + ->where('invoices.is_recurring', '=', true) ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'total', 'frequencies.name as frequency', 'start_date', 'end_date'); if ($clientPublicId) { @@ -322,10 +322,20 @@ class InvoiceController extends \BaseController { $invoice = Invoice::scope($publicId)->with('account.country', 'client', 'invoice_items')->firstOrFail(); Utils::trackViewed($invoice->invoice_number . ' - ' . $invoice->client->name, ENTITY_INVOICE); + $contactIds = DB::table('invitations') + ->join('contacts', 'contacts.id', '=','invitations.contact_id') + ->where('invitations.invoice_id', '=', $invoice->id) + ->where('invitations.account_id', '=', Auth::user()->account_id) + ->where('invitations.deleted_at', '=', null) + ->select('contacts.public_id')->lists('public_id'); + $data = array( 'account' => $invoice->account, 'invoice' => $invoice, 'method' => 'PUT', + 'invitationContactIds' => $contactIds, + 'clientSizes' => ClientSize::orderBy('id')->get(), + 'clientIndustries' => ClientIndustry::orderBy('name')->get(), 'url' => 'invoices/' . $publicId, 'title' => '- ' . $invoice->invoice_number, 'client' => $invoice->client); @@ -349,6 +359,8 @@ class InvoiceController extends \BaseController { 'invoiceNumber' => $invoiceNumber, 'method' => 'POST', 'url' => 'invoices', + 'clientSizes' => ClientSize::orderBy('id')->get(), + 'clientIndustries' => ClientIndustry::orderBy('name')->get(), 'title' => '- New Invoice', 'client' => $client, 'items' => json_decode(Input::old('items'))); @@ -393,20 +405,18 @@ class InvoiceController extends \BaseController { { return InvoiceController::bulk(); } - - $rules = array( - 'client' => 'required', - ); - $validator = Validator::make(Input::all(), $rules); - if ($validator->fails()) { + $input = json_decode(Input::get('data')); + $inputClient = $input->client; + $inputClient->name = trim($inputClient->name); + + if (!$inputClient->name) { return Redirect::to('invoices/create') - ->withInput() - ->withErrors($validator); + ->withInput(); } else { - $clientPublicId = Input::get('client'); - + $clientPublicId = $input->client->public_id; + if ($clientPublicId == "-1") { $client = Client::createNew(); @@ -418,24 +428,58 @@ class InvoiceController extends \BaseController { $client = Client::scope($clientPublicId)->with('contacts')->firstOrFail(); $contact = $client->contacts()->where('is_primary', '=', true)->firstOrFail(); } - - $client->name = trim(Input::get('name')); - $client->work_phone = trim(Input::get('work_phone')); - $client->address1 = trim(Input::get('address1')); - $client->address2 = trim(Input::get('address2')); - $client->city = trim(Input::get('city')); - $client->state = trim(Input::get('state')); - $client->postal_code = trim(Input::get('postal_code')); - if (Input::get('country_id')) { - $client->country_id = Input::get('country_id'); - } + + $inputClient = $input->client; + $client->name = trim($inputClient->name); + $client->work_phone = trim($inputClient->work_phone); + $client->address1 = trim($inputClient->address1); + $client->address2 = trim($inputClient->address2); + $client->city = trim($inputClient->city); + $client->state = trim($inputClient->state); + $client->postal_code = trim($inputClient->postal_code); + $client->country_id = $inputClient->country_id ? $inputClient->country_id : null; $client->save(); - $contact->first_name = trim(Input::get('first_name')); - $contact->last_name = trim(Input::get('last_name')); - $contact->phone = trim(Input::get('phone')); - $contact->email = trim(Input::get('email')); - $client->contacts()->save($contact); + $isPrimary = true; + $contacts = []; + $contactIds = []; + $sendInvoiceIds = []; + + foreach ($inputClient->contacts as $contact) + { + if (isset($contact->public_id) && $contact->public_id) + { + $record = Contact::scope($contact->public_id)->firstOrFail(); + } + else + { + $record = Contact::createNew(); + } + + $record->email = trim($contact->email); + $record->first_name = trim($contact->first_name); + $record->last_name = trim($contact->last_name); + $record->phone = trim($contact->phone); + $record->is_primary = $isPrimary; + $isPrimary = false; + + $client->contacts()->save($record); + $contacts[] = $record; + $contactIds[] = $record->public_id; + + if ($contact->send_invoice) + { + $sendInvoiceIds[] = $record->id; + } + } + + foreach ($client->contacts as $contact) + { + if (!in_array($contact->public_id, $contactIds)) + { + $contact->forceDelete(); + } + } if ($publicId) { $invoice = Invoice::scope($publicId)->firstOrFail(); @@ -445,20 +489,22 @@ class InvoiceController extends \BaseController { } $invoice->client_id = $client->id; - $invoice->discount = Input::get('discount'); - $invoice->invoice_number = trim(Input::get('invoice_number')); - $invoice->invoice_date = Utils::toSqlDate(Input::get('invoice_date')); - $invoice->due_date = Utils::toSqlDate(Input::get('due_date')); + $invoice->discount = $input->discount; + $invoice->invoice_number = trim($input->invoice_number); + $invoice->invoice_date = Utils::toSqlDate($input->invoice_date); + $invoice->due_date = Utils::toSqlDate($input->due_date); - $invoice->frequency_id = Input::get('recurring') ? Input::get('frequency') : 0; - $invoice->start_date = Utils::toSqlDate(Input::get('start_date')); - $invoice->end_date = Utils::toSqlDate(Input::get('end_date')); - $invoice->notes = Input::get('notes'); - $invoice->po_number = Input::get('po_number'); + $invoice->is_recurring = $input->is_recurring; + $invoice->frequency_id = $input->frequency_id ? $input->frequency_id : 0; + $invoice->start_date = Utils::toSqlDate($input->start_date); + $invoice->end_date = Utils::toSqlDate($input->end_date); + $invoice->notes = $input->notes; + $invoice->po_number = $input->po_number; + $client->invoices()->save($invoice); - $items = json_decode(Input::get('items')); + $items = $input->invoice_items; $total = 0; foreach ($items as $item) @@ -484,6 +530,24 @@ class InvoiceController extends \BaseController { $invoice->total = $total; $invoice->save(); + foreach ($contacts as $contact) + { + $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); + + if (in_array($contact->id, $sendInvoiceIds) && !$invitation) + { + $invitation = Invitation::createNew(); + $invitation->invoice_id = $invoice->id; + $invitation->contact_id = $contact->id; + $invitation->invitation_key = str_random(20); + $invitation->save(); + } + else if (!in_array($contact->id, $sendInvoiceIds) && $invitation) + { + $invitation->forceDelete(); + } + } + foreach ($items as $item) { if (!$item->cost && !$item->qty && !$item->product_key && !$item->notes) @@ -529,7 +593,7 @@ class InvoiceController extends \BaseController { } else if ($action == 'email') { - $this->mailer->sendInvoice($invoice, $contact); + $this->mailer->sendInvoice($invoice); Session::flash('message', 'Successfully emailed invoice'); } else { @@ -594,12 +658,12 @@ class InvoiceController extends \BaseController { $invoice = Invoice::with('invoice_items')->scope($publicId)->firstOrFail(); $clone = Invoice::createNew(); - foreach (['client_id', 'discount', 'invoice_date', 'due_date', 'frequency_id', 'start_date', 'end_date', 'notes'] as $field) + foreach (['client_id', 'discount', 'invoice_date', 'due_date', 'is_recurring', 'frequency_id', 'start_date', 'end_date', 'notes'] as $field) { $clone->$field = $invoice->$field; } - if (!$clone->isRecurring()) + if (!$clone->is_recurring) { $clone->invoice_number = Auth::user()->account->getNextInvoiceNumber(); } diff --git a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php index e2e375f58185..0a855849db23 100755 --- a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php +++ b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php @@ -204,7 +204,7 @@ class ConfideSetupUsersTable extends Migration { $t->string('last_name'); $t->string('email'); $t->string('phone'); - $t->timestamp('last_login'); + $t->timestamp('last_login'); $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users'); @@ -238,14 +238,15 @@ class ConfideSetupUsersTable extends Migration { $t->string('invoice_number'); $t->float('discount'); $t->string('po_number'); - $t->date('invoice_date'); + $t->date('invoice_date')->nullable(); $t->date('due_date')->nullable(); $t->text('notes'); + $t->boolean('is_recurring'); $t->unsignedInteger('frequency_id'); $t->date('start_date')->nullable(); $t->date('end_date')->nullable(); - $t->date('last_sent_date')->nullable(); + $t->timestamp('last_sent_date')->nullable(); $t->unsignedInteger('recurring_invoice_id')->index()->nullable(); @@ -274,6 +275,7 @@ class ConfideSetupUsersTable extends Migration { $t->timestamps(); $t->softDeletes(); + $t->timestamp('sent_date'); $t->timestamp('viewed_date'); $t->foreign('user_id')->references('id')->on('users'); diff --git a/app/mailers/ContactMailer.php b/app/mailers/ContactMailer.php index 0eeed2235252..19714d596b57 100755 --- a/app/mailers/ContactMailer.php +++ b/app/mailers/ContactMailer.php @@ -8,30 +8,24 @@ use Auth; class ContactMailer extends Mailer { - public function sendInvoice(Invoice $invoice, Contact $contact) + public function sendInvoice(Invoice $invoice) { $view = 'invoice'; $data = array('link' => URL::to('view') . '/' . $invoice->invoice_key); $subject = ''; - if (Auth::check()) { - $invitation = Invitation::createNew(); - } else { - $invitation = Invitation::createNew($invoice); - } + foreach ($invoice->invitations as $invitation) + { + //$invitation->date_sent = + $invitation->save(); - $invitation->invoice_id = $invoice->id; - $invitation->user_id = Auth::check() ? Auth::user()->id : $invoice->user_id; - $invitation->contact_id = $contact->id; - $invitation->invitation_key = str_random(20); - $invitation->save(); + $this->sendTo($invitation->contact->email, $subject, $view, $data); + } if (!$invoice->isSent()) { $invoice->invoice_status_id = INVOICE_STATUS_SENT; $invoice->save(); } - - return $this->sendTo($contact->email, $subject, $view, $data); } } \ No newline at end of file diff --git a/app/models/Activity.php b/app/models/Activity.php index c0190a074728..d16bbeb18972 100755 --- a/app/models/Activity.php +++ b/app/models/Activity.php @@ -66,7 +66,7 @@ class Activity extends Eloquent { $userName = Auth::check() ? Auth::user()->getFullName() : 'System'; - if ($invoice->isRecurring()) { + if ($invoice->is_recurring) { $message = $userName . ' created ' . link_to('invoices/'.$invoice->public_id, 'recuring invoice'); } else { $message = $userName . ' created invoice ' . link_to('invoices/'.$invoice->public_id, $invoice->invoice_number); diff --git a/app/models/Invitation.php b/app/models/Invitation.php index e95e08a30641..72f9196de7b7 100644 --- a/app/models/Invitation.php +++ b/app/models/Invitation.php @@ -2,7 +2,7 @@ class Invitation extends EntityModel { - protected $hidden = array('id', 'created_at', 'updated_at', 'deleted_at', 'viewed_date'); + protected $hidden = array('id', 'account_id', 'user_id', 'contact_id', 'created_at', 'updated_at', 'deleted_at', 'viewed_date'); public function invoice() { diff --git a/app/models/Invoice.php b/app/models/Invoice.php index 44f74a0c266a..274df9f0e5d9 100755 --- a/app/models/Invoice.php +++ b/app/models/Invoice.php @@ -24,6 +24,11 @@ class Invoice extends EntityModel return $this->belongsTo('InvoiceStatus'); } + public function invitations() + { + return $this->hasMany('Invitation'); + } + public function getName() { return $this->invoice_number; @@ -34,11 +39,6 @@ class Invoice extends EntityModel return ENTITY_INVOICE; } - public function isRecurring() - { - return $this->frequency_id > 0; - } - public function isSent() { return $this->invoice_status_id >= INVOICE_STATUS_SENT; diff --git a/app/views/header.blade.php b/app/views/header.blade.php index f5d95fd1833e..ecae9e300b2e 100755 --- a/app/views/header.blade.php +++ b/app/views/header.blade.php @@ -207,6 +207,10 @@ cursor: move !important; } + .closer-row { + margin-bottom: 2px; + } + /* Animate col width changes */ .row div { diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index b45db2311602..053526a82340 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -15,61 +15,62 @@ 'client' => 'required', 'product_key' => 'max:14', )); }} - - - - @if ($invoice) - {{ Former::populate($invoice); }} - {{ Former::populateField('id', $invoice->public_id); }} - {{ Former::populateField('invoice_date', Utils::fromSqlDate($invoice->invoice_date)); }} - {{ Former::populateField('due_date', Utils::fromSqlDate($invoice->due_date)); }} - {{ Former::populateField('start_date', Utils::fromSqlDate($invoice->start_date)); }} - {{ Former::populateField('end_date', Utils::fromSqlDate($invoice->end_date)); }} - @else - {{ Former::populateField('invoice_number', $invoiceNumber) }} - {{ Former::populateField('invoice_date', date('m/d/Y')) }} - {{ Former::populateField('start_date', date('m/d/Y')) }} - {{ Former::populateField('frequency', FREQUENCY_MONTHLY) }} - @endif -
+
- {{ Former::select('client')->addOption('', '')->fromQuery($clients, 'name', 'public_id')->select($client ? $client->public_id : '')->addGroupClass('client_select') - ->help('Create new client') }} + {{ Former::select('client')->addOption('', '')->fromQuery($clients, 'name', 'public_id')->data_bind("dropdown: client") + ->addGroupClass('client_select closer-row') }} + +
+
+ +
+
+ +
+
+
+ +
+
+
{{ Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'") }} - {{ Former::textarea('notes') }} + {{ Former::textarea('notes')->data_bind("value: notes, valueUpdate: 'afterkeydown'") }}
-
- {{ Former::checkbox('recurring')->text('Enable | Learn more')->onchange('toggleRecurring()') +
+ {{ Former::checkbox('recurring')->text('Enable | Learn more')->data_bind("checked: is_recurring") ->inlineHelp($invoice && $invoice->last_sent_date ? 'Last invoice sent ' . Utils::timestampToDateString($invoice->last_sent_date) : '') }}
-
- {{ Former::text('invoice_number')->label('Invoice #') }} - {{ Former::text('po_number')->label('PO number') }} - {{ Former::text('invoice_date') }} - {{ Former::text('due_date') }} - - + @if ($invoice && $invoice->recurring_invoice_id) +
+ Created by a {{ link_to('/invoices/'.$invoice->recurring_invoice_id, 'recurring invoice') }} +
+ @endif +
+ {{ Former::text('invoice_number')->label('Invoice #')->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") }} + {{ Former::text('po_number')->label('PO number')->data_bind("value: po_number, valueUpdate: 'afterkeydown'") }} + {{ Former::text('invoice_date')->data_bind("value: invoice_date, valueUpdate: 'afterkeydown'") }} + {{ Former::text('due_date')->data_bind("value: due_date, valueUpdate: 'afterkeydown'") }} + {{-- Former::text('invoice_date')->label('Invoice Date')->data_date_format('yyyy-mm-dd') --}}
- -

 

- {{ Former::hidden('items')->data_bind("value: ko.toJSON(items)") }} + {{ Former::hidden('data')->data_bind("value: ko.toJSON(model)") }} @@ -77,16 +78,16 @@ - - + + - + @@ -189,55 +190,63 @@ -
-
- - {{ Former::legend('Organization') }} - {{ Former::text('name') }} - {{ Former::text('work_phone')->label('Phone') }} - - {{ Former::legend('Contact') }} - {{ Former::text('first_name') }} - {{ Former::text('last_name') }} - {{ Former::text('email') }} - {{ Former::text('phone') }} +
+
+ {{ Former::legend('Organization') }} + {{ Former::text('name')->data_bind("value: name, valueUpdate: 'afterkeydown'") }} + {{ Former::text('work_phone')->data_bind("value: work_phone, valueUpdate: 'afterkeydown'")->label('Phone') }} + + + {{ Former::legend('Address') }} + {{ Former::text('address1')->label('Street')->data_bind("value: address1, valueUpdate: 'afterkeydown'") }} + {{ Former::text('address2')->label('Apt/Floor')->data_bind("value: address2, valueUpdate: 'afterkeydown'") }} + {{ Former::text('city')->data_bind("value: city, valueUpdate: 'afterkeydown'") }} + {{ Former::text('state')->data_bind("value: state, valueUpdate: 'afterkeydown'") }} + {{ Former::text('postal_code')->data_bind("value: postal_code, valueUpdate: 'afterkeydown'") }} + {{ Former::select('country_id')->addOption('','')->label('Country') + ->fromQuery($countries, 'name', 'id')->data_bind("value: country_id") }} +
-
- {{ Former::legend('Address') }} - {{ Former::text('address1')->label('Street') }} - {{ Former::text('address2')->label('Apt/Floor') }} - {{ Former::text('city') }} - {{ Former::text('state') }} - {{ Former::text('postal_code') }} - {{ Former::select('country_id')->addOption('','')->label('Country')->addGroupClass('country_select') - ->fromQuery($countries, 'name', 'id')->select($client ? $client->country_id : '') }} +
+ + {{ Former::legend('Contacts') }} +
+ {{ Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") }} + {{ Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") }} + {{ Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") }} + {{ Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown'") }} + {{ Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") }} + +
+
+ + {{ link_to('#', 'Remove contact', array('data-bind'=>'click: $parent.removeContact')) }} + + + {{ link_to('#', 'Add contact', array('data-bind'=>'click: $parent.addContact')) }} + +
+
+
+ + {{ Former::legend('Additional Info') }} + {{ Former::select('client_size_id')->addOption('','')->label('Size') + ->fromQuery($clientSizes, 'name', 'id')->select($client ? $client->client_size_id : '') }} + {{ Former::select('client_industry_id')->addOption('','')->label('Industry') + ->fromQuery($clientIndustries, 'name', 'id')->select($client ? $client->client_industry_id : '') }} + {{ Former::textarea('notes') }} +
-
@@ -247,17 +256,6 @@ {{ Former::close() }} - @stop \ No newline at end of file diff --git a/public/js/script.js b/public/js/script.js index 5fc03ac6d665..d9635634119a 100755 --- a/public/js/script.js +++ b/public/js/script.js @@ -1,6 +1,6 @@ function generatePDF(invoice) { var invoiceNumber = invoice.invoice_number; - var issuedOn = invoice.invoice_date; + var issuedOn = invoice.invoice_date ? invoice.invoice_date : ''; var amount = '$0.00'; var marginLeft = 90; @@ -560,4 +560,40 @@ function convertDataURIToBinary(dataURI) { array[i] = raw.charCodeAt(i); } return array; -} \ No newline at end of file +} + + +ko.bindingHandlers.dropdown = { + init: function (element, valueAccessor, allBindingsAccessor) { + var options = allBindingsAccessor().dropdownOptions|| {}; + var value = ko.utils.unwrapObservable(valueAccessor()); + var id = (value && value.public_id) ? value.public_id() : (value && value.id) ? value.id() : false; + if (id) $(element).val(id); + console.log("combo-init: %s", id); + $(element).combobox(options); + + /* + ko.utils.registerEventHandler(element, "change", function () { + console.log("change: %s", $(element).val()); + var + valueAccessor($(element).val()); + //$(element).combobox('refresh'); + }); + */ + }, + update: function (element, valueAccessor) { + var value = ko.utils.unwrapObservable(valueAccessor()); + var id = (value && value.public_id) ? value.public_id() : (value && value.id) ? value.id() : false; + console.log("combo-update: %s", id); + if (id) $(element).val(id); + $(element).combobox('refresh'); + } +}; + + +var CONSTS = {}; +CONSTS.INVOICE_STATUS_DRAFT = 1; +CONSTS.INVOICE_STATUS_SENT = 2; +CONSTS.INVOICE_STATUS_VIEWED = 3; +CONSTS.INVOICE_STATUS_PARTIAL = 4; +CONSTS.INVOICE_STATUS_PAID = 5;
Item DescriptionRateUnitsUnit CostQuantity Line Total
- + {{ Former::text('product_key')->useDatalist(Product::getProductKeys($products), 'key')->onkeyup('onChange()') @@ -110,7 +111,7 @@ -   +