mirror of
https://github.com/itflow-org/itflow
synced 2026-11-15 00:47:24 +00:00
Trips modernized for MSP use: rebuilt trips list with stat cards, pills (mine, for a client, for a ticket, our own), driver/client/date filters, search across site and ticket, footer total, bulk Set Driver/Set Client/Delete and export with driver, round trip, one way, site, ticket and odometer columns; new trip quick look with route, Google Maps directions, driver, client, site and ticket links, odometer, logged by and trips to the site, with Edit, Copy, Return Trip and Delete; one shared form for New, Edit, Copy and Return Trip with client sites and tickets, site address as destination, ticket fills purpose and site, known routes fill miles, one way miles with round trip total and optional odometer readings; Log a trip and a Trips card on tickets, a Trips tab on locations, Mileage report rows open the trip and show site and ticket with Site and Ticket CSV columns; every agent can log and manage their own trips while others' trips need Financial write and deleting them Financial full; trips follow ticket merge, client move and ticket or location delete; new functions/trips.php and handlers rewritten around tripSave and tripDelete, trip_model.php removed; demo data links trips to sites and onsite tickets; database 3.0.3 adds trip_location_id, trip_ticket_id and trip_created_by with backfill, makes trip_miles decimal and renames trip_end_odmeter to trip_end_odometer; CHANGELOG updated
This commit is contained in:
50
CHANGELOG.md
50
CHANGELOG.md
@@ -6,9 +6,9 @@ This file documents all notable changes made to ITFlow.
|
||||
|
||||
### Upgrading to 26.11
|
||||
|
||||
Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.0.2) in the same pass. Nothing else is required.
|
||||
Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.0.3) in the same pass. Nothing else is required.
|
||||
|
||||
This is a big release, with twenty-three database steps in one go, so take a backup before you update.
|
||||
This is a big release, with twenty-four database steps in one go, so take a backup before you update.
|
||||
|
||||
The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists:
|
||||
|
||||
@@ -25,6 +25,8 @@ Expenses already entered take who entered them from the audit log, where it has
|
||||
|
||||
Recurring expenses keep their next date and from then on run on that day of the month - monthly ones stay monthly and yearly ones yearly. Look over their next dates once after the update (see Breaking Changes, Expenses). The expenses they recorded before are linked to them, matched on the vendor, category, client and description. No expense or recurring expense becomes billable to a client by itself - they stay your cost until you mark them (see New Features, Expenses).
|
||||
|
||||
Trips already logged keep the miles they were entered with (see Breaking Changes, Trips). Each takes who logged it from the audit log, or its driver where the log has no record. A trip for a client whose destination is one of that client's locations, by name or by address, is linked to that location.
|
||||
|
||||
If you are coming from 26.08 or earlier, the database update still has to be run from the command line one time, exactly as described under Upgrading to 26.09 below.
|
||||
|
||||
### Breaking Changes and Notes
|
||||
@@ -88,6 +90,10 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- Deleting a recurring expense needs full Financial access, the same as deleting an expense. It used to need write access.
|
||||
- Unbilled Time also lists expenses to bill for agents with Financial access, and its count in the menu adds them. For those agents it shows in the menu even with ticketing turned off.
|
||||
- Transactions has a Reimbursement type for money paid back to an agent. A Transactions export filtered to Expense leaves those out.
|
||||
- Trips:
|
||||
- Every agent can open Trips when accounting is on, and log their own driving. An agent without Financial access sees only the trips they drove or logged, and can change or delete only those. Logging a trip for someone else, or changing theirs, needs write access to Financial, and deleting theirs needs full access. Before, any agent who opened the page saw every trip of their clients and could change any of them.
|
||||
- A trip's miles are the whole trip. The form asks for the miles one way and doubles them for a round trip. The old form saved the number as typed, whether Round Trip was ticked or not, so an older round trip entered as the miles one way still counts once. Edit those to correct them.
|
||||
- The database column `trips.trip_end_odmeter` is now `trip_end_odometer`. Anything of your own that reads it, such as a custom report, needs the new name.
|
||||
- Notifications now follow access too. An agent only sees notifications for clients and areas they can open, so someone without sales access no longer sees invoice and quote notifications.
|
||||
- Drop-downs: Tom Select is gone, replaced by ITFlow's own picker. It works the way you are used to: type to search, tick several in a multi-select, and type a new value in fields that allow it, such as ticket watchers. See Developer Updates if you have custom pages that used Tom Select.
|
||||
- Several small windows are gone because the change is now made where the value is shown:
|
||||
@@ -529,7 +535,29 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- The MFA QR code is drawn on the page itself.
|
||||
- You can see your remembered browsers and forget one or all of them.
|
||||
- You can set how many rows a list shows per page.
|
||||
- **Trips**: the driver shows as an agent pill, like ticket assignees.
|
||||
- **Trips** (rebuilt)
|
||||
- Along the top of the list: the miles this month against the same days of last month, the year so far, your own this month, and how much of the year's driving was for clients.
|
||||
- Pills cut the list to your own trips, trips for a client, trips for a ticket and our own errands, each with a count. Filter by driver, client and date. Search finds what it was for, where from and to, the client, site, driver and ticket number. The total of everything the filters leave sits under the list.
|
||||
- Each row shows the route with the ticket and what it was for, the client and their site, the driver as an agent pill, and the miles with a mark for a round trip.
|
||||
- Clicking a trip opens its quick look:
|
||||
- the route, with Directions in Google Maps from where it started, and the miles each way for a round trip
|
||||
- the driver, the client, their site and the ticket, each linked
|
||||
- the odometer readings, and who logged it and when
|
||||
- how many trips have gone to that site, opening them
|
||||
- From the quick look or a row's menu: Edit, Copy (dated today), Return Trip and Delete. Return Trip is the way back, from there to where it started, one way and on the same day.
|
||||
- New Trip, Edit, Copy and Return Trip are one form with an icon on every field:
|
||||
- Pick the client to choose from their sites and their tickets (open ones, and those done in the last 60 days). A site fills in its address as where to. A ticket fills in what the trip was for, and its site.
|
||||
- From starts where your last trip started, and offers the places you start from most.
|
||||
- Enter the miles one way and switch Round trip on or off. The form shows what the whole trip comes to.
|
||||
- A route you have driven before fills in its miles.
|
||||
- Odometer readings are optional. With both, the trip is what the odometer says.
|
||||
- Tickets: Log a trip in the ticket's menu starts with its client, site and subject. An onsite ticket, or one with trips, has a Trips card listing them with the miles driven for it.
|
||||
- Locations: the location page has a Trips tab with the trips out to it and Log Trip.
|
||||
- The Mileage report shows each trip's ticket and site under its purpose, a trip's date opens it, and the CSV has Site and Ticket columns.
|
||||
- Bulk actions set the driver or the client of the ticked trips, or delete them. Changing the client takes off the old client's site and ticket.
|
||||
- Export takes the same filters as the list, with Driver, Round Trip, One Way, Site, Ticket and the odometer readings as columns.
|
||||
- Trips follow their ticket. Merging a ticket with its replies moves its trips to the ticket it goes into, and moving it to another client takes its trips along. Deleting a ticket or a location keeps the trips, without the link.
|
||||
- The demo data logs a trip to the site for each onsite ticket fixed in the last six months.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -597,7 +625,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
|
||||
### Developer Updates
|
||||
|
||||
- Database 2.8.0 to 2.9.8, one file per step under `admin/database_updates/`, with `db.sql` matching 2.9.8:
|
||||
- Database 2.8.0 to 3.0.3, one file per step under `admin/database_updates/`, with `db.sql` matching 3.0.3:
|
||||
- 2.8.0: `racks.rack_units_descending`, `rack_power_capacity` and `rack_photo_rear`. On `rack_units`: `unit_face`, `unit_full_depth`, `unit_type`, `unit_power_watts`, `unit_notes` and a `unit_asset_id` key. Unlinked items move from asset id 0 to NULL.
|
||||
- 2.8.1: `location_documents` and `location_files`.
|
||||
- 2.8.2: `vendor_tags` (tag type 6), and `vendor_contact_phone_country_code`, `vendor_contact_phone` and `vendor_contact_email`.
|
||||
@@ -633,6 +661,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- 3.0.0 (after 2.9.9, the way 2.0.0 followed 1.9.9): `recurring_expenses.recurring_expense_frequency` becomes `varchar(10)` (`month`, `quarter` or `year`, from 1 and 2), with `recurring_expense_anchor_day` (backfilled from the next date) and `recurring_expense_end_date`. `recurring_expense_day` and `recurring_expense_month` are dropped. `expenses.expense_recurring_expense_id`, backfilled by vendor, category, client and description from the day each recurring expense was set up.
|
||||
- 3.0.1: `expenses.expense_billable` (0), `expense_markup` (`decimal(7,2)`, 0.00), `expense_invoice_id` (keyed) and `expense_invoice_item_id`. `recurring_expenses.recurring_expense_billable` (0), `recurring_expense_markup` and `recurring_expense_recurring_invoice_id`. `settings.config_expense_markup` (0.00).
|
||||
- 3.0.2: `expenses.expense_import_key` (keyed), `expense_paid_by` (0) and `expense_reimbursement_id` (keyed). `expense_import_rules` (`import_rule_match` unique, `import_rule_vendor_id`, `import_rule_category_id`, `import_rule_client_id`, `import_rule_billable`, `import_rule_used_count`). `reimbursements` (`reimbursement_user_id`, `reimbursement_date`, `reimbursement_amount`, `reimbursement_currency_code`, `reimbursement_account_id`, `reimbursement_payment_method`, `reimbursement_reference`, `reimbursement_expense_id`, `reimbursement_created_by`). `accounts.account_import_map`.
|
||||
- 3.0.3: `trips.trip_end_odmeter` renamed `trip_end_odometer`, and `trip_miles` from `float(15,1)` to `decimal(10,1)` (0.0). New `trip_location_id`, `trip_ticket_id` and `trip_created_by` (0), keyed with `trip_user_id` (`trip_user_id`, `trip_date`). `trip_created_by` is backfilled from the audit log's Trip Create entries, else the driver, and `trip_location_id` where the destination matches one of the client's locations by name or by `formatAddress()` of its address, case and spacing aside.
|
||||
- New shared function files, loaded by `functions.php`: `functions/asset.php`, `asset_interface.php`, `certificate.php`, `client_overview.php`, `contact.php`, `credential.php`, `dashboard.php`, `links.php`, `location.php`, `notifications.php`, `project.php`, `rack.php`, `search.php`, `service.php`, `software.php`, `tags.php`, `ticket.php`, `ticket_fields.php`, `user.php` and `vendor.php`. They hold the queries and rules, so the pages, the API and cron share one definition.
|
||||
- Removed post models: `agent/post/{asset_interface,certificate,credential,domain,event,location,network_ip,network,recurring_ticket,vendor}_model.php`, `admin/post/user_model.php` and `api/v1/certificates/certificate_model.php`. Their handlers call the shared functions above. Interface handlers moved to `agent/post/asset_interface.php`.
|
||||
- New scripts: `js/itflow_select.js`, `quick_pick.js`, `tag_picker.js`, `global_search.js`, `notifications.js`, `dashboard.js`, `client_overview.js`, `client_switcher.js`, `credentials.js` and `autosubmit.js`, and `agent/js/calendar.js`, `calendar_event_modal.js`, `files.js`, `interface_form.js`, `network_form.js`, `project_modal.js` and `ticket_schedule_modal.js`.
|
||||
@@ -813,6 +842,19 @@ If you are coming from 26.08 or earlier, the database update still has to be run
|
||||
- `expenseRow()` adds `paid_by_name`, `reimbursement_date`, `reimbursed_account_name`, `reimbursement_out_id` and `reimbursed_to_name` (its joins on `accounts` and `users` are aliased now). `expenseShows()` adds `to_reimburse`, and `expenseFiltersSql()` takes `paid_by`.
|
||||
- Handlers: POST `expense_reimburse` (`user_id`, `expense_ids[]`, `account`, `date`, `payment_method`, `reference`) and GET `expense_reimbursement_delete` (Financial at 3). `bulk_edit_expenses` with `field=account` clears `expense_paid_by` and skips paid back ones. The export adds `expense_paid_by_display`.
|
||||
- New windows `agent/modals/expense/expense_reimburse.php` (`?user_id=` or `?expense_ids[]=`) and `expense_reimbursement.php?id=`. `agent/transactions.php`, its export and `agent/post/transaction.php` give the payment the type Reimbursement, with the agent as the other side. `dashboardAttention()` adds Expenses to reimburse.
|
||||
- Trips, in the new `functions/trips.php` (loaded by `functions.php`):
|
||||
- Access: `tripCanSeeAll()` (Financial at 1), `tripCanManage()` (Financial at 2), `tripIsMine($row)` (drove or logged it), `tripCanEdit($row)`, `tripCanDelete($row)` (theirs, or Financial at 3) and `tripScopeSql()` - `clientScopeSql('trips.trip_client_id')`, plus their own trips only without Financial access. Every read below goes through it.
|
||||
- `tripRow($id, $scoped = true)` reads a trip with its client, driver (`driver_name`, `driver_avatar`), `created_by_name`, location and ticket, through `tripJoinsSql()` (`users` aliased `driver`). `tripsFor($column, $id, $limit)` and `tripTotalsFor($column, $id)` read the trips of a ticket, location or client, and `tripTableHtml($rows)` draws them for a location's Trips tab.
|
||||
- `tripSave($in, $id, &$error)` is the one path that writes a trip from a form - New, Edit, Copy and Return. It takes `date`, `driver`, `client_id`, `ticket_id`, `location_id`, `source`, `destination`, `miles` (one way), `round_trip`, `odometer_start`, `odometer_end` and `purpose`. `trip_miles` is the whole trip: both odometer readings make it, otherwise the miles one way, doubled for a round trip. A ticket brings its client (and needs Support access), a location and ticket must be the client's, and without `tripCanManage()` the driver is the agent. It sets `trip_created_by` on insert. `tripDelete($id, &$error)` checks `tripCanDelete()`.
|
||||
- The list and export share `tripFiltersSql($src, &$summary)` (q, driver, client, location_id, ticket_id, show, dtf and dtt) and `tripShows()`, the pills (`mine`, `client`, `ticket` and `own`). Also `tripStats()`, `tripDrivers()`, `tripClientSites()`, `tripClientTickets()`, `tripKnownRoutes()` (one way miles by lowercased `from|to`, both directions, the latest first), `tripStartPlaces()`, `tripLastStart()`, `tripOfficeAddress()`, `tripLocationAddress()`, `tripOneWay()`, `tripLabel()`, `tripMapUrl()`, `tripUnit()` and `tripMilesText()`.
|
||||
- New windows: the quick look `agent/modals/trip/trip.php?id=`, `trip_form.php` (the fields of `trip_add.php`, `trip_edit.php` and `trip_copy.php`), `trip_bulk_edit.php?field=` (driver or client) and `trip_bulk_delete.php`, with the menu in `agent/includes/trip_bulk_menu.php`. `trip_add.php` takes `?client_id=`, `?location_id=` and `?ticket_id=`, and `trip_copy.php` takes `?return=1`.
|
||||
- Handlers in `agent/post/trip.php`: `add_trip` and `edit_trip` (with `trip_id`) post the fields above through `tripSave()`. GET `delete_trip`. New are `bulk_edit_trips` (`field`, `value`, `trip_ids[]`; Financial at 2) and `bulk_delete_trips` (Financial at 3). `export_trips` takes the list's filters and adds the `user_name`, `round_trip_display`, `trip_one_way`, `location_name`, `ticket_display`, `trip_start_odometer` and `trip_end_odometer` columns. The old fields `roundtrip` and `user` are gone.
|
||||
- `agent/trips.php` takes `?driver=`, `?client=`, `?location_id=`, `?ticket_id=`, `?show=` and `?open=`, and is open to every agent. `side_nav.php` lists Trips under Finance for everyone, `globalSearchPages()` offers it with accounting on, and `inc_all_client.php` counts `$num_trips` through `tripTotalsFor()`.
|
||||
- `agent/ticket.php` has the Trips card and Log a trip. `agent/location.php` has the Trips tab. `agent/reports/mileage.php` joins the location and ticket and links each date to `trips.php?open=`.
|
||||
- Kept in step: `ticketDeleteForGood()` and `locationClearReferences()` clear `trip_ticket_id` and `trip_location_id`. `ticketMergeInto()` with `$move_replies` moves the trips. `change_client_ticket` moves the ticket's trips to the new client, without a location.
|
||||
- Seeds: `demoBuildClient()` gives the quarterly visits the primary location's address and id, and adds a trip for each onsite ticket resolved in the last six months. Every seeded trip has `trip_created_by`.
|
||||
- CSS: `.itflow-trip-route`.
|
||||
- Removed: `agent/post/trip_model.php`.
|
||||
- The billing helpers (`billingTimeInvoice()`, `quoteConvert()`, `recurringInvoiceGenerate()` and the rest) take the agent's name as plain text. Their handlers now pass `stripslashes($session_name)`, so a name with an apostrophe no longer shows a backslash in history and the audit log.
|
||||
- Sidebar brand: `agent/css/sidebar_brand_fix.css` and `agent/js/sidebar_brand_fix.js` from 26.10 are replaced by CSS in `css/itflow_custom.css`. `includes/load_company_settings.php` now sets `$session_company_logo`.
|
||||
- `alertMessageHtml()` no longer double-encodes, and save messages built from `escapeSql()` values use `stripslashes()` at the flash.
|
||||
|
||||
38
admin/database_updates/3.0.3.php
Normal file
38
admin/database_updates/3.0.3.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.0.3 (from 3.0.2)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Trips (functions/trips.php): the client's site it went to, the ticket it
|
||||
// was for, who logged it, miles as an exact number, and the end odometer
|
||||
// reading's misspelt column put right
|
||||
mysqli_query($mysqli, "ALTER TABLE `trips`
|
||||
CHANGE `trip_end_odmeter` `trip_end_odometer` int(11) DEFAULT NULL,
|
||||
MODIFY `trip_miles` decimal(10,1) NOT NULL DEFAULT 0.0,
|
||||
ADD COLUMN `trip_location_id` int(11) NOT NULL DEFAULT 0 AFTER `trip_client_id`,
|
||||
ADD COLUMN `trip_ticket_id` int(11) NOT NULL DEFAULT 0 AFTER `trip_location_id`,
|
||||
ADD COLUMN `trip_created_by` int(11) NOT NULL DEFAULT 0 AFTER `trip_ticket_id`,
|
||||
ADD KEY `trip_user_id` (`trip_user_id`, `trip_date`),
|
||||
ADD KEY `trip_ticket_id` (`trip_ticket_id`),
|
||||
ADD KEY `trip_location_id` (`trip_location_id`)");
|
||||
|
||||
// Who logged each trip, from the audit log where it says - otherwise the driver
|
||||
mysqli_query($mysqli, "UPDATE trips JOIN logs ON log_type = 'Trip' AND log_action = 'Create' AND log_entity_id = trip_id
|
||||
SET trip_created_by = log_user_id WHERE trip_created_by = 0 AND log_user_id > 0");
|
||||
mysqli_query($mysqli, "UPDATE trips SET trip_created_by = trip_user_id WHERE trip_created_by = 0");
|
||||
|
||||
// New Trip put a client site's address in as the destination - link those
|
||||
// trips to the site, by its address or its name
|
||||
mysqli_query($mysqli, "UPDATE trips JOIN locations ON location_client_id = trip_client_id
|
||||
SET trip_location_id = location_id
|
||||
WHERE trip_location_id = 0 AND trip_client_id > 0 AND (
|
||||
LOWER(TRIM(trip_destination)) = LOWER(TRIM(location_name))
|
||||
OR LOWER(TRIM(trip_destination)) = LOWER(TRIM(CONCAT_WS(' ',
|
||||
NULLIF(TRIM(location_address), ''),
|
||||
NULLIF(IF(TRIM(COALESCE(location_city, '')) <> '' AND TRIM(CONCAT_WS(' ', NULLIF(TRIM(location_state), ''), NULLIF(TRIM(location_zip), ''))) <> '',
|
||||
CONCAT(TRIM(location_city), ', ', TRIM(CONCAT_WS(' ', NULLIF(TRIM(location_state), ''), NULLIF(TRIM(location_zip), '')))),
|
||||
TRIM(CONCAT_WS(' ', NULLIF(TRIM(location_city), ''), NULLIF(TRIM(location_state), ''), NULLIF(TRIM(location_zip), '')))), '')))))");
|
||||
@@ -1676,6 +1676,11 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
|
||||
}
|
||||
}
|
||||
|
||||
// The driving out to them (functions/trips.php): the quarterly visit to
|
||||
// their main site, and a trip for each onsite ticket fixed in the last six
|
||||
// months - to the ticket's site, on the day it was fixed, by who fixed it
|
||||
$trip_miles = 8.5 + ($index * 2.5);
|
||||
$main_site = $profile['sites'][0];
|
||||
for ($trip_month = min(6, $months); $trip_month >= 0; $trip_month = $trip_month - 3) {
|
||||
if (demoIsFuture(demoMonthDate($trip_month, 17))) {
|
||||
continue;
|
||||
@@ -1684,14 +1689,38 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) {
|
||||
'trip_date' => demoMonthDate($trip_month, 17),
|
||||
'trip_purpose' => 'Onsite - ' . $profile['onsite_visit'],
|
||||
'trip_source' => 'Office',
|
||||
'trip_destination' => $profile['sites'][0]['city'] . ', ' . $profile['sites'][0]['state'],
|
||||
'trip_miles' => 8.5 + ($index * 2.5),
|
||||
'trip_destination' => formatAddress($main_site['address'], $main_site['city'], $main_site['state'], $main_site['zip'], '', ' '),
|
||||
'trip_miles' => $trip_miles,
|
||||
'round_trip' => 1,
|
||||
'trip_user_id' => $user_id,
|
||||
'trip_created_by' => $user_id,
|
||||
'trip_client_id' => $client_id,
|
||||
'trip_location_id' => $primary_location_id,
|
||||
'trip_created_at' => demoMonthDateTime($trip_month, 17, 17, 30),
|
||||
]);
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_subject, ticket_assigned_to, ticket_resolved_at, location_id, location_name, location_address, location_city, location_state, location_zip
|
||||
FROM tickets LEFT JOIN locations ON location_id = ticket_location_id
|
||||
WHERE ticket_client_id = " . intval($client_id) . " AND ticket_onsite = 1 AND ticket_resolved_at IS NOT NULL
|
||||
AND ticket_resolved_at >= NOW() - INTERVAL 6 MONTH AND ticket_resolved_at <= NOW()
|
||||
ORDER BY ticket_resolved_at ASC");
|
||||
while ($ticket = mysqli_fetch_assoc($sql)) {
|
||||
$driver_id = intval($ticket['ticket_assigned_to']) ?: $user_id;
|
||||
starterInsert($mysqli, 'trips', [
|
||||
'trip_date' => substr($ticket['ticket_resolved_at'], 0, 10),
|
||||
'trip_purpose' => mb_substr('Onsite - ' . $ticket['ticket_subject'], 0, 200),
|
||||
'trip_source' => 'Office',
|
||||
'trip_destination' => $ticket['location_id'] !== null ? tripLocationAddress($ticket) : formatAddress($main_site['address'], $main_site['city'], $main_site['state'], $main_site['zip'], '', ' '),
|
||||
'trip_miles' => $trip_miles,
|
||||
'round_trip' => 1,
|
||||
'trip_user_id' => $driver_id,
|
||||
'trip_created_by' => $driver_id,
|
||||
'trip_client_id' => $client_id,
|
||||
'trip_location_id' => intval($ticket['location_id']),
|
||||
'trip_ticket_id' => intval($ticket['ticket_id']),
|
||||
'trip_created_at' => $ticket['ticket_resolved_at'],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3655,6 +3684,7 @@ function demoBuildInternalDiary($mysqli, $context, &$counts) {
|
||||
'trip_miles' => $errand[2],
|
||||
'round_trip' => 1,
|
||||
'trip_user_id' => $user_id,
|
||||
'trip_created_by' => $user_id,
|
||||
'trip_client_id' => 0,
|
||||
'trip_created_at' => demoDateTime($days_ago, 17, 45),
|
||||
]);
|
||||
|
||||
@@ -271,8 +271,8 @@ if (isset($_GET['client_id'])) {
|
||||
AND (COALESCE(event_end, event_start) >= CURDATE() OR (event_repeat <> '' AND (event_repeat_until IS NULL OR event_repeat_until >= CURDATE())))"));
|
||||
$num_calendar_events = intval($row['num']);
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(trip_id) AS num FROM trips WHERE trip_archived_at IS NULL AND trip_client_id = $client_id"));
|
||||
$num_trips = intval($row['num']);
|
||||
// The trips this agent may see - all of them with Financial access, else their own
|
||||
[$num_trips] = tripTotalsFor('trip_client_id', $client_id);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +213,15 @@
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/agent/trips.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "trips.php") { echo "active"; } ?>">
|
||||
<i class="nav-icon fas fa-route"></i>
|
||||
<p>Trips</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php // Everyone logs their driving - without Financial access, their own trips ?>
|
||||
<li class="nav-item">
|
||||
<a href="/agent/trips.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "trips.php") { echo "active"; } ?>">
|
||||
<i class="nav-icon fas fa-route"></i>
|
||||
<p>Trips</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php if (lookupUserPermission("module_financial") >= 1) { ?>
|
||||
<li class="nav-item">
|
||||
<a href="/agent/accounts.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "accounts.php") { echo "active"; } ?>">
|
||||
<i class="nav-icon fas fa-piggy-bank"></i>
|
||||
|
||||
20
agent/includes/trip_bulk_menu.php
Normal file
20
agent/includes/trip_bulk_menu.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/*
|
||||
* The trips list's Bulk menu - shown once trips are ticked (js/bulk_actions.js).
|
||||
* Set the driver or client of the ticked trips, or delete them.
|
||||
*/
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
?>
|
||||
<div class="dropdown" id="bulkActionButton" hidden>
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-layer-group"></i><span class="d-none d-xxl-inline ms-1">Bulk</span> (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/trip/trip_bulk_edit.php?field=driver" data-bulk="true"><i class="fas fa-fw fa-user text-secondary me-2"></i>Set Driver</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/trip/trip_bulk_edit.php?field=client" data-bulk="true"><i class="fas fa-fw fa-building text-secondary me-2"></i>Set Client</a>
|
||||
<?php if (lookupUserPermission('module_financial') >= 3) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold ajax-modal" href="#" data-modal-url="modals/trip/trip_bulk_delete.php" data-bulk="true"><i class="fas fa-fw fa-trash me-2"></i>Delete</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,6 +104,11 @@ if (!$location || ($client_url && intval($location['location_client_id']) !== in
|
||||
$tickets = ticketRows("tickets.ticket_location_id = $location_id", max(25, $ticket_open_count));
|
||||
}
|
||||
|
||||
// The driving out to it - functions/trips.php, so an agent without Financial access sees their own
|
||||
$show_trips = !empty($config_module_enable_accounting);
|
||||
$trips = $show_trips ? tripsFor('trip_location_id', $location_id, 25) : [];
|
||||
[$trip_count, $trip_miles] = $show_trips ? tripTotalsFor('trip_location_id', $location_id) : [0, 0.0];
|
||||
|
||||
$documents = $can_support ? linkedRecords('location', $location_id, 'document') : [];
|
||||
$files = $can_support ? linkedRecords('location', $location_id, 'file') : [];
|
||||
$history = locationHistory($location_id, 100);
|
||||
@@ -114,10 +119,14 @@ if (!$location || ($client_url && intval($location['location_client_id']) !== in
|
||||
'networks' => ['label' => 'Networks', 'icon' => 'fa-network-wired', 'count' => count($networks)],
|
||||
'racks' => ['label' => 'Racks', 'icon' => 'fa-server', 'count' => count($racks)],
|
||||
'tickets' => ['label' => 'Tickets', 'icon' => 'fa-life-ring', 'count' => $ticket_count, 'badge' => $ticket_open_count],
|
||||
'trips' => ['label' => 'Trips', 'icon' => 'fa-route', 'count' => $trip_count],
|
||||
'documents' => ['label' => 'Documents', 'icon' => 'fa-file-alt', 'count' => count($documents)],
|
||||
'files' => ['label' => 'Files', 'icon' => 'fa-paperclip', 'count' => count($files)],
|
||||
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => count($history)],
|
||||
];
|
||||
if (!$show_trips) {
|
||||
unset($location_tabs['trips']);
|
||||
}
|
||||
if (!$can_support) {
|
||||
unset($location_tabs['assets'], $location_tabs['networks'], $location_tabs['racks'], $location_tabs['tickets'], $location_tabs['documents'], $location_tabs['files']);
|
||||
}
|
||||
@@ -211,7 +220,7 @@ if (!$location || ($client_url && intval($location['location_client_id']) !== in
|
||||
<?php } ?>
|
||||
<?php if (!empty($config_module_enable_accounting)) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/trip/trip_add.php?client_id=<?= $client_id ?>&location_id=<?= $location_id ?>">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip_add.php?client_id=<?= $client_id ?>&location_id=<?= $location_id ?>">
|
||||
<i class="fa fa-fw fa-route text-secondary me-2"></i>Trip here
|
||||
</a>
|
||||
<?php } ?>
|
||||
@@ -607,6 +616,26 @@ if (!$location || ($client_url && intval($location['location_client_id']) !== in
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Trips out to it -->
|
||||
<?php if ($show_trips) { ?>
|
||||
<div class="tab-pane fade" id="location-pane-trips" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small"><?= $trip_count ? escapeHtml(tripMilesText($trip_miles)) . ' driven out here, newest first.' : 'The driving out here, once it is logged.' ?></span>
|
||||
<?php if (!$location_archived_at) { ?>
|
||||
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip_add.php?client_id=<?= $client_id ?>&location_id=<?= $location_id ?>"><i class="fas fa-plus me-2"></i>Log Trip</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($trips) { ?>
|
||||
<?= tripTableHtml($trips) ?>
|
||||
<?php if ($trip_count > count($trips)) { ?>
|
||||
<div class="small text-body-secondary px-3 py-2 border-top">The <?= count($trips) ?> most recent of <?= $trip_count ?> - <a href="trips.php?client_id=<?= $client_id ?>&location_id=<?= $location_id ?>">all of them in Trips</a>.</div>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<div class="itflow-asset-empty">No trips to this site.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- History, from the audit log -->
|
||||
<div class="tab-pane fade" id="location-pane-history" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
|
||||
144
agent/modals/trip/trip.php
Normal file
144
agent/modals/trip/trip.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* A trip's quick look - where from and where to with a directions link, the
|
||||
* miles (each way for a round trip), the client and their site, the ticket
|
||||
* it was for, what it was for, who drove and who logged it, and the
|
||||
* odometer readings. Edit, Copy, Return Trip and Delete. Opened from the
|
||||
* trips list, a ticket, a site and the Mileage report.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$row = tripRow(intval($_GET['id'] ?? 0));
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That trip no longer exists, or is not one you can see.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$trip_id = intval($row['trip_id']);
|
||||
$client_id = intval($row['trip_client_id']);
|
||||
$miles = floatval($row['trip_miles']);
|
||||
$round = intval($row['round_trip']) === 1;
|
||||
$can_edit = tripCanEdit($row);
|
||||
$can_delete = tripCanDelete($row);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark" data-bs-theme="dark">
|
||||
<div class="d-flex align-items-center gap-3 min-w-0 flex-grow-1">
|
||||
<span class="itflow-asset-icon itflow-asset-icon-lg bg-primary-subtle text-primary-emphasis" aria-hidden="true"><i class="fas fa-route"></i></span>
|
||||
<div class="min-w-0 flex-grow-1">
|
||||
<h5 class="modal-title fw-semibold text-break"><?= escapeHtml($row['trip_destination']) ?></h5>
|
||||
<div class="small text-body-secondary mt-1 text-truncate"><?= escapeHtml(date('D, M j, Y', strtotime($row['trip_date']))) ?> · from <?= escapeHtml($row['trip_source']) ?></div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="fs-4 fw-semibold text-white text-nowrap"><?= escapeHtml(tripMilesText($miles)) ?></div>
|
||||
<div class="small text-body-secondary"><?= $round ? 'round trip' : 'one way' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white ms-3" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- The route -->
|
||||
<div class="d-flex align-items-stretch gap-3 mb-3 p-3 rounded border bg-body-tertiary">
|
||||
<div class="d-flex flex-column align-items-center pt-1" aria-hidden="true">
|
||||
<i class="fas fa-circle text-body-secondary small"></i>
|
||||
<div class="flex-grow-1 border-start border-2 my-1" style="border-style: dashed !important;"></div>
|
||||
<i class="fas fa-map-marker-alt text-danger"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 min-w-0 d-flex flex-column gap-3">
|
||||
<div>
|
||||
<div class="small text-body-secondary">From</div>
|
||||
<div class="text-break"><?= escapeHtml($row['trip_source']) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-body-secondary">To<?= $row['location_name'] !== null ? ' - ' . escapeHtml($row['location_name']) : '' ?></div>
|
||||
<div class="text-break"><?= escapeHtml($row['trip_destination']) ?></div>
|
||||
<?php if ($round) { ?>
|
||||
<div class="small text-body-secondary mt-1"><i class="fas fa-sync-alt me-1"></i>and back - <?= escapeHtml(tripMilesText(tripOneWay($row))) ?> each way</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<a class="btn btn-outline-secondary" href="<?= escapeHtml(tripMapUrl($row)) ?>" target="_blank" rel="noopener"><i class="fas fa-map-marked-alt me-2"></i>Directions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- At a glance -->
|
||||
<div class="row g-2 itflow-asset-glance">
|
||||
<div class="col-sm-6">
|
||||
<div class="itflow-glance-item" title="Driver">
|
||||
<i class="fas fa-fw fa-user"></i>
|
||||
<span><?= formatAssignee($row['trip_user_id'], $row['driver_name'] ?? '', $row['driver_avatar'] ?? '', false, 'No driver') ?></span>
|
||||
</div>
|
||||
<div class="itflow-glance-item" title="Client">
|
||||
<i class="fas fa-fw fa-building"></i>
|
||||
<?php if ($client_id) { ?>
|
||||
<a class="text-truncate" href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($row['client_name']) ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">Our own - not for a client</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($row['location_name'] !== null) { ?>
|
||||
<div class="itflow-glance-item" title="Their site">
|
||||
<i class="fas fa-fw fa-map-marker-alt"></i>
|
||||
<a class="text-truncate" href="location.php?client_id=<?= $client_id ?>&location_id=<?= intval($row['trip_location_id']) ?>"><?= escapeHtml($row['location_name']) ?></a>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (intval($row['trip_ticket_id'])) { ?>
|
||||
<div class="itflow-glance-item" title="Ticket">
|
||||
<i class="fas fa-fw fa-life-ring"></i>
|
||||
<span class="text-truncate"><?= $row['ticket_number'] !== null ? ticketLink(['ticket_id' => $row['trip_ticket_id'], 'ticket_client_id' => $row['ticket_client_id'], 'ticket_prefix' => $row['ticket_prefix'], 'ticket_number' => $row['ticket_number'], 'ticket_subject' => $row['ticket_subject'], 'ticket_resolved_at' => $row['ticket_resolved_at'], 'ticket_closed_at' => $row['ticket_closed_at']]) : '<span class="text-body-secondary">A ticket since deleted</span>' ?></span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<?php if ($row['trip_start_odometer'] !== null || $row['trip_end_odometer'] !== null) { ?>
|
||||
<div class="itflow-glance-item" title="Odometer">
|
||||
<i class="fas fa-fw fa-tachometer-alt"></i>
|
||||
<span class="font-monospace"><?= $row['trip_start_odometer'] !== null ? number_format(intval($row['trip_start_odometer'])) : '?' ?> → <?= $row['trip_end_odometer'] !== null ? number_format(intval($row['trip_end_odometer'])) : '?' ?></span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="itflow-glance-item" title="Logged">
|
||||
<i class="fas fa-fw fa-clock"></i>
|
||||
<span><span title="<?= escapeHtml($row['trip_created_at']) ?>"><?= timeAgo($row['trip_created_at']) ?></span><?php if ($row['created_by_name'] && intval($row['trip_created_by']) !== intval($row['trip_user_id'])) { ?> <span class="text-body-secondary">by <?= escapeHtml($row['created_by_name']) ?></span><?php } ?></span>
|
||||
</div>
|
||||
<?php if ($client_id || intval($row['trip_location_id'])) {
|
||||
[$site_trips, $site_miles] = intval($row['trip_location_id']) ? tripTotalsFor('trip_location_id', $row['trip_location_id']) : tripTotalsFor('trip_client_id', $client_id); ?>
|
||||
<div class="itflow-glance-item" title="Trips there">
|
||||
<i class="fas fa-fw fa-route"></i>
|
||||
<a class="text-truncate" href="trips.php?client_id=<?= $client_id ?><?= intval($row['trip_location_id']) ? '&location_id=' . intval($row['trip_location_id']) : '' ?>"><?= $site_trips ?> trip<?= $site_trips === 1 ? '' : 's' ?> to <?= intval($row['trip_location_id']) ? 'this site' : 'them' ?></a>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="small fw-semibold text-uppercase text-body-secondary mb-1">What for</div>
|
||||
<div class="text-break" style="white-space: pre-line;"><?= escapeHtml($row['trip_purpose']) ?></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-primary text-bold ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip_edit.php?id=<?= $trip_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
|
||||
<?php } ?>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip_copy.php?id=<?= $trip_id ?>&return=1"><i class="fas fa-undo-alt me-2"></i>Return Trip</button>
|
||||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip_copy.php?id=<?= $trip_id ?>"><i class="fas fa-copy me-2"></i>Copy</button>
|
||||
</div>
|
||||
<?php if ($can_delete) { ?>
|
||||
<a class="btn btn-outline-danger confirm-link" href="post.php?delete_trip=<?= $trip_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||||
data-confirm-text="<?= escapeHtml(tripLabel($row)) ?> goes for good."><i class="fas fa-trash"></i><span class="d-none d-sm-inline ms-2">Delete</span></a>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,137 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* New Trip. ?client_id= starts it with a client, ?location_id= with their
|
||||
* site as where to (from a location's page), ?ticket_id= with the ticket it
|
||||
* is for - its client, site and subject as the purpose (Log Trip on a
|
||||
* ticket).
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$client_id = intval($_GET['client_id'] ?? 0);
|
||||
// Opened from a location's page - that's where the trip is to
|
||||
$trip_location_id = intval($_GET['location_id'] ?? 0);
|
||||
$form_row = null;
|
||||
$form_mode = 'new';
|
||||
$form_client_id = intval($_GET['client_id'] ?? 0);
|
||||
$form_location_id = intval($_GET['location_id'] ?? 0);
|
||||
$form_ticket_id = intval($_GET['ticket_id'] ?? 0);
|
||||
$form_purpose = '';
|
||||
if ($form_ticket_id) {
|
||||
$ticket = ticketRow($form_ticket_id);
|
||||
if ($ticket && intval($ticket['ticket_client_id']) && hasClientAccess(intval($ticket['ticket_client_id']))) {
|
||||
$form_client_id = intval($ticket['ticket_client_id']);
|
||||
$form_location_id = $form_location_id ?: intval($ticket['ticket_location_id']);
|
||||
$form_purpose = mb_substr('Onsite - ' . $ticket['ticket_subject'], 0, 200);
|
||||
} else {
|
||||
$form_ticket_id = 0;
|
||||
}
|
||||
}
|
||||
if ($form_client_id) {
|
||||
$client_id = $form_client_id;
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-route me-2"></i>New Trip</h5>
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-route me-2"></i><?= $form_ticket_id ? 'Log Trip <span class="fw-normal text-white-50 ms-1">' . escapeHtml(ticketReference($ticket)) . '</span>' : 'New Trip' ?></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php if ($client_id) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php }else{ ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Client</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id" required>
|
||||
<option value="0">- Client (Optional) -</option>
|
||||
<?php
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at is NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option value="<?= $client_id_select ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name ?></option>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="mb-3 col">
|
||||
<label>Date <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= date("Y-m-d") ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col">
|
||||
<label>Miles <strong class="text-danger">*</strong> / <span class="text-secondary">Roundtrip</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-bicycle"></i></span>
|
||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,1}" name="miles" placeholder="0.0" required autofocus>
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="roundtrip" value="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Location <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-marker-alt"></i></span>
|
||||
<input type="text" class="form-control" name="source" placeholder="Enter your starting location" maxlength="200" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-arrow-right"></i></span>
|
||||
<select class="form-select select2" name="destination" data-tags="true" data-placeholder="- Select or Enter a Destination -" required>
|
||||
<option value=""></option>
|
||||
<?php
|
||||
if ($client_id) {
|
||||
$sql_locations = mysqli_query($mysqli, "SELECT location_id, location_address, location_city, location_name, location_state, location_zip FROM locations WHERE location_archived_at IS NULL AND location_client_id = $client_id ORDER BY location_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_locations)) {
|
||||
$location_name = escapeHtml($row['location_name']);
|
||||
$location_address = escapeHtml($row['location_address']);
|
||||
$location_city = escapeHtml($row['location_city']);
|
||||
$location_state = escapeHtml($row['location_state']);
|
||||
$location_zip = escapeHtml($row['location_zip']);
|
||||
?>
|
||||
<option <?= intval($row['location_id']) === $trip_location_id ? 'selected' : '' ?>><?= formatAddress($location_address, $location_city, $location_state, $location_zip, '', ' ') ?></option>
|
||||
<?php
|
||||
}
|
||||
} ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purpose <strong class="text-danger">*</strong></label>
|
||||
<textarea rows="4" class="form-control" placeholder="Enter a purpose" name="purpose" maxlength="200" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Driver</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="user" required>
|
||||
<option value="">- Driver -</option>
|
||||
<?php
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users
|
||||
WHERE user_type = 1 AND user_archived_at IS NULL ORDER BY user_name ASC"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$user_id = intval($row['user_id']);
|
||||
$user_name = escapeHtml($row['user_name']);
|
||||
?>
|
||||
<option <?php if ($session_user_id == $user_id) { echo "selected"; } ?> value="<?= $user_id ?>"<?= selectOptionUser($row['user_id'], $row['user_name'], $row['user_avatar'] ?? '') ?>><?= $user_name ?></option>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/trip_form.php'; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_trip" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
<button type="submit" name="add_trip" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Log Trip</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
45
agent/modals/trip/trip_bulk_delete.php
Normal file
45
agent/modals/trip/trip_bulk_delete.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/* Delete the ticked trips. */
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_financial', 3);
|
||||
|
||||
$trip_ids = array_values(array_unique(array_filter(array_map('intval', (array) ($_GET['trip_ids'] ?? [])))));
|
||||
$count = count($trip_ids);
|
||||
if (!$count) {
|
||||
echo json_encode(['error' => 'Tick the trips to delete first.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-trash me-2"></i>Delete <?= $count ?> Trip<?= $count === 1 ? '' : 's' ?></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<?php foreach ($trip_ids as $trip_id) { ?>
|
||||
<input type="hidden" name="trip_ids[]" value="<?= $trip_id ?>">
|
||||
<?php } ?>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex gap-3 align-items-start">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-danger"></i>
|
||||
<div>
|
||||
<div class="fw-semibold"><?= $count === 1 ? 'This trip goes' : "These $count trips go" ?> for good.</div>
|
||||
<div class="text-body-secondary small mt-1">Their miles come out of the Mileage report and off their tickets and sites. This can't be undone.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_delete_trips" class="btn btn-danger text-bold"><i class="fas fa-trash me-2"></i>Delete <?= $count ?></button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
77
agent/modals/trip/trip_bulk_edit.php
Normal file
77
agent/modals/trip/trip_bulk_edit.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Set the driver or client of the ticked trips - ?field= driver or client.
|
||||
* A new client takes the site and ticket off a trip that was another
|
||||
* client's. Posts bulk_edit_trips with field, value and trip_ids[].
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_financial', 2);
|
||||
|
||||
$trip_ids = array_values(array_unique(array_filter(array_map('intval', (array) ($_GET['trip_ids'] ?? [])))));
|
||||
$count = count($trip_ids);
|
||||
$field = (string) ($_GET['field'] ?? '');
|
||||
|
||||
$fields = [
|
||||
'driver' => ['label' => 'Driver', 'icon' => 'fa-user'],
|
||||
'client' => ['label' => 'Client', 'icon' => 'fa-building'],
|
||||
];
|
||||
if (!isset($fields[$field])) {
|
||||
echo json_encode(['error' => 'Pick what to set from the Bulk Action menu.']);
|
||||
exit;
|
||||
}
|
||||
if (!$count) {
|
||||
echo json_encode(['error' => 'Tick the trips to change first.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw <?= $fields[$field]['icon'] ?> me-2"></i>Set <?= $fields[$field]['label'] ?> <span class="fw-normal text-white-50 ms-1"><?= $count ?> trip<?= $count === 1 ? '' : 's' ?></span></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="field" value="<?= $field ?>">
|
||||
<?php foreach ($trip_ids as $trip_id) { ?>
|
||||
<input type="hidden" name="trip_ids[]" value="<?= $trip_id ?>">
|
||||
<?php } ?>
|
||||
<div class="modal-body">
|
||||
<label class="form-label"><?= $fields[$field]['label'] ?><?= $field === 'driver' ? ' <strong class="text-danger">*</strong>' : '' ?></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw <?= $fields[$field]['icon'] ?>"></i></span>
|
||||
<?php if ($field === 'driver') { ?>
|
||||
<select class="form-select select2" name="value" required>
|
||||
<option value="">- Pick who drove -</option>
|
||||
<?php foreach (tripDrivers() as $user_id => $driver) { ?>
|
||||
<option value="<?= $user_id ?>"<?= selectOptionUser($user_id, $driver['user_name'], $driver['user_avatar'] ?? '') ?>><?= escapeHtml($driver['user_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<?php } else { ?>
|
||||
<select class="form-select select2" name="value">
|
||||
<option value="0">- Our own - not for a client -</option>
|
||||
<?php
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) { ?>
|
||||
<option value="<?= intval($row['client_id']) ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($field === 'client') { ?>
|
||||
<div class="form-text">A trip that was another client's loses its site and ticket - they were that client's.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="bulk_edit_trips" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Set on <?= $count ?></button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,161 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copy a trip - the same route, client and purpose, dated today, driven by
|
||||
* you. With ?return=1 it is the Return Trip: from where it went back to where
|
||||
* it started, one way, on the same day.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_financial', 2);
|
||||
|
||||
$trip_id = intval($_GET['id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT round_trip, trip_archived_at, trip_client_id, trip_created_at, trip_date, trip_destination,
|
||||
trip_miles, trip_purpose, trip_source, trip_user_id FROM trips WHERE trip_id = $trip_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$trip_date = escapeHtml($row['trip_date']);
|
||||
$trip_purpose = escapeHtml($row['trip_purpose']);
|
||||
$trip_source = escapeHtml($row['trip_source']);
|
||||
$trip_destination = escapeHtml($row['trip_destination']);
|
||||
$trip_miles = number_format(floatval($row['trip_miles']),1);
|
||||
$trip_user_id = intval($row['trip_user_id']);
|
||||
$trip_created_at = escapeHtml($row['trip_created_at']);
|
||||
$trip_archived_at = escapeHtml($row['trip_archived_at']);
|
||||
$round_trip = escapeHtml($row['round_trip']);
|
||||
$client_id = intval($row['trip_client_id']);
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
$form_row = tripRow(intval($_GET['id'] ?? 0));
|
||||
if (!$form_row) {
|
||||
exit(json_encode(['error' => 'That trip no longer exists, or is not one you can see.']));
|
||||
}
|
||||
$form_mode = !empty($_GET['return']) ? 'return' : 'copy';
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-copy me-2"></i>Copying Trip</h5>
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-<?= $form_mode === 'return' ? 'undo-alt' : 'copy' ?> me-2"></i><?= $form_mode === 'return' ? 'Return Trip' : 'Copy Trip' ?></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row g-2">
|
||||
|
||||
<div class="mb-3 col">
|
||||
<label>Date <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= date("Y-m-d") ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col">
|
||||
<label>Miles <strong class="text-danger">*</strong> / <span class="text-secondary">Roundtrip</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-bicycle"></i></span>
|
||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,1}" name="miles" value="<?= $trip_miles ?>" placeholder="0.0" required>
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="roundtrip" value="1" <?php if ($round_trip == 1) { echo "checked"; } ?>>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Location <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-map-marker-alt"></i></span>
|
||||
<input type="text" class="form-control" name="source" maxlength="200" value="<?= $trip_source ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-arrow-right"></i></span>
|
||||
<select class="form-select select2" name="destination" data-tags="true" data-placeholder="- Select / Input Destination -" required>
|
||||
<option><?= $trip_destination ?></option>
|
||||
<?php
|
||||
|
||||
$sql_locations_select = mysqli_query($mysqli, "SELECT location_address, location_city, location_name, location_state, location_zip FROM locations WHERE location_archived_at IS NULL AND location_client_id = $client_id ORDER BY location_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_locations_select)) {
|
||||
$location_name = escapeHtml($row['location_name']);
|
||||
$location_address = escapeHtml($row['location_address']);
|
||||
$location_city = escapeHtml($row['location_city']);
|
||||
$location_state = escapeHtml($row['location_state']);
|
||||
$location_zip = escapeHtml($row['location_zip']);
|
||||
$location_full_address = formatAddress($location_address, $location_city, $location_state, $location_zip, '', ' ');
|
||||
|
||||
?>
|
||||
<option><?= $location_full_address ?></option>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purpose <strong class="text-danger">*</strong></label>
|
||||
<textarea rows="4" class="form-control" placeholder="Enter a purpose" name="purpose" maxlength="200" required><?= $trip_purpose ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Driver</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="user" required>
|
||||
<option value="">- Driver -</option>
|
||||
<?php
|
||||
|
||||
$sql_users = mysqli_query($mysqli, "SELECT users.user_id, user_name, user_avatar FROM users
|
||||
LEFT JOIN user_settings on users.user_id = user_settings.user_id
|
||||
WHERE user_role_id > 1 AND user_archived_at IS NULL ORDER BY user_name ASC"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql_users)) {
|
||||
$user_id_select = intval($row['user_id']);
|
||||
$user_name_select = escapeHtml($row['user_name']);
|
||||
?>
|
||||
<option <?php if ($trip_user_id == $user_id_select) { echo "selected"; } ?> value="<?= $user_id_select ?>"<?= selectOptionUser($row['user_id'], $row['user_name'], $row['user_avatar'] ?? '') ?>><?= $user_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_GET['client_id'])) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Client</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id">
|
||||
<option value="">- Client (Optional) -</option>
|
||||
<?php
|
||||
|
||||
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_clients)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name_select = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option <?php if ($client_id == $client_id_select) { echo "selected"; } ?> value="<?= $client_id_select ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php require __DIR__ . '/trip_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_trip" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Copy</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
<button type="submit" name="add_trip" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Log Trip</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -1,163 +1,36 @@
|
||||
<?php
|
||||
|
||||
/* Edit a trip - your own, or anyone's with write access to Financial. */
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_financial', 2);
|
||||
|
||||
$trip_id = intval($_GET['id']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT round_trip, trip_archived_at, trip_client_id, trip_created_at, trip_date, trip_destination,
|
||||
trip_miles, trip_purpose, trip_source, trip_user_id FROM trips WHERE trip_id = $trip_id LIMIT 1");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$trip_date = escapeHtml($row['trip_date']);
|
||||
$trip_purpose = escapeHtml($row['trip_purpose']);
|
||||
$trip_source = escapeHtml($row['trip_source']);
|
||||
$trip_destination = escapeHtml($row['trip_destination']);
|
||||
$trip_miles = number_format(floatval($row['trip_miles']),1);
|
||||
$trip_user_id = intval($row['trip_user_id']);
|
||||
$trip_created_at = escapeHtml($row['trip_created_at']);
|
||||
$trip_archived_at = escapeHtml($row['trip_archived_at']);
|
||||
$round_trip = escapeHtml($row['round_trip']);
|
||||
$client_id = intval($row['trip_client_id']);
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
$form_row = tripRow(intval($_GET['id'] ?? 0));
|
||||
if (!$form_row) {
|
||||
exit(json_encode(['error' => 'That trip no longer exists, or is not one you can see.']));
|
||||
}
|
||||
if (!tripCanEdit($form_row)) {
|
||||
exit(json_encode(['error' => "That is someone else's trip - changing it needs write access to Financial."]));
|
||||
}
|
||||
$form_mode = 'edit';
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-route me-2"></i>Editing Trip</h5>
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-route me-2"></i>Edit Trip <span class="fw-normal text-white-50 ms-1"><?= escapeHtml($form_row['trip_date']) ?></span></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= intval($form_row['trip_id']) ?>">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= $trip_id ?>">
|
||||
|
||||
<div class="row g-2">
|
||||
|
||||
<div class="mb-3 col">
|
||||
<label>Date <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= $trip_date ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col">
|
||||
<label>Miles <strong class="text-danger">*</strong> / <span class="text-secondary">Roundtrip</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-bicycle"></i></span>
|
||||
<input type="text" class="form-control" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,1}" name="miles" value="<?= $trip_miles ?>" placeholder="0.0" required>
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input" type="checkbox" name="roundtrip" value="1" <?php if ($round_trip == 1) { echo "checked"; } ?>>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Location <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-map-marker-alt"></i></span>
|
||||
<input type="text" class="form-control" name="source" maxlength="200" value="<?= $trip_source ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-arrow-right"></i></span>
|
||||
<select class="form-select select2" name="destination" data-tags="true" data-placeholder="- Select / Input Destination -" required>
|
||||
<option><?= $trip_destination ?></option>
|
||||
<?php
|
||||
|
||||
$sql_locations_select = mysqli_query($mysqli, "SELECT location_address, location_city, location_name, location_state, location_zip FROM locations WHERE location_archived_at IS NULL AND location_client_id = $client_id ORDER BY location_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_locations_select)) {
|
||||
$location_name = escapeHtml($row['location_name']);
|
||||
$location_address = escapeHtml($row['location_address']);
|
||||
$location_city = escapeHtml($row['location_city']);
|
||||
$location_state = escapeHtml($row['location_state']);
|
||||
$location_zip = escapeHtml($row['location_zip']);
|
||||
$location_full_address = formatAddress($location_address, $location_city, $location_state, $location_zip, '', ' ');
|
||||
|
||||
?>
|
||||
<option><?= $location_full_address ?></option>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purpose <strong class="text-danger">*</strong></label>
|
||||
<textarea rows="4" class="form-control" name="purpose" placeholder="Enter a purpose" maxlength="200" required><?= $trip_purpose ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Driver</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="user" required>
|
||||
<option value="">- Driver -</option>
|
||||
<?php
|
||||
|
||||
$sql_users = mysqli_query($mysqli, "SELECT users.user_id, user_name, user_avatar FROM users
|
||||
LEFT JOIN user_settings on users.user_id = user_settings.user_id
|
||||
WHERE (users.user_id = $trip_user_id) OR (user_archived_at IS NULL AND user_status = 1) ORDER BY user_name ASC"
|
||||
);
|
||||
while ($row = mysqli_fetch_assoc($sql_users)) {
|
||||
$user_id_select = intval($row['user_id']);
|
||||
$user_name_select = escapeHtml($row['user_name']);
|
||||
?>
|
||||
<option <?php if ($trip_user_id == $user_id_select) { echo "selected"; } ?> value="<?= $user_id_select ?>"<?= selectOptionUser($row['user_id'], $row['user_name'], $row['user_avatar'] ?? '') ?>><?= $user_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_GET['client_id'])) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Client</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id">
|
||||
<option value="">- Client (Optional) -</option>
|
||||
<?php
|
||||
|
||||
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_clients)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name_select = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option <?php if ($client_id == $client_id_select) { echo "selected"; } ?> value="<?= $client_id_select ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<?php require __DIR__ . '/trip_form.php'; ?>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_trip" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
<button type="submit" name="edit_trip" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Export the trips - pre-filled from the trips list's filters, and editable:
|
||||
* the export runs whatever this form posts. Agents without Financial access
|
||||
* export the trips they drove or logged.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_financial');
|
||||
|
||||
// Pre-filled from the trips page's current filters. Everything here is editable -
|
||||
// the export runs whatever this form posts, not whatever the page happened to show.
|
||||
$client_id = intval($_GET['client_id'] ?? 0);
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
$q_filter = $_GET['q'] ?? '';
|
||||
$driver_filter = intval($_GET['driver'] ?? 0);
|
||||
$client_filter = intval($_GET['client'] ?? 0);
|
||||
$location_filter = intval($_GET['location_id'] ?? 0);
|
||||
$ticket_filter = intval($_GET['ticket_id'] ?? 0);
|
||||
$shows = tripShows();
|
||||
$show_filter = isset($shows[$_GET['show'] ?? '']) ? (string) ($_GET['show'] ?? '') : '';
|
||||
$see_all = tripCanSeeAll();
|
||||
|
||||
// Date Filter - the all-time sentinels from filter_header.php leave the fields blank
|
||||
// The all-time sentinels from filter_header.php leave the dates blank
|
||||
$date_from_filter = (!empty($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') ? escapeHtml($_GET['dtf']) : '';
|
||||
$date_to_filter = (!empty($_GET['dtt']) && $_GET['dtt'] !== '2099-12-31') ? escapeHtml($_GET['dtt']) : '';
|
||||
|
||||
$location_name = $location_filter ? getFieldById('locations', $location_filter, 'location_name') : null;
|
||||
$ticket_row = $ticket_filter ? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject FROM tickets WHERE ticket_id = $ticket_filter")) : null;
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -37,27 +48,85 @@ ob_start();
|
||||
|
||||
<?php exportTabsFiltersOpen(); ?>
|
||||
|
||||
<?php if (!$see_all) { ?>
|
||||
<div class="small text-body-secondary mb-3"><i class="fas fa-user me-1"></i>The trips you drove or logged</div>
|
||||
<?php } ?>
|
||||
<?php if ($location_name !== null) { ?>
|
||||
<input type="hidden" name="location_id" value="<?= $location_filter ?>">
|
||||
<div class="small text-body-secondary mb-3"><i class="fas fa-map-marker-alt me-1"></i>Only the trips to <?= escapeHtml($location_name) ?></div>
|
||||
<?php } ?>
|
||||
<?php if ($ticket_row) { ?>
|
||||
<input type="hidden" name="ticket_id" value="<?= $ticket_filter ?>">
|
||||
<div class="small text-body-secondary mb-3"><i class="fas fa-life-ring me-1"></i>Only the trips for <?= escapeHtml(ticketReference($ticket_row) . ' - ' . $ticket_row['ticket_subject']) ?></div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Search</label>
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Purpose, source, destination">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Purpose, from, to, client, site, driver, ticket">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Dated From</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="dtf" value="<?= $date_from_filter ?>" max="2999-12-31">
|
||||
<div class="row g-3 mb-3">
|
||||
<?php if ($see_all) { ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Driver</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="driver">
|
||||
<option value="">- Anyone -</option>
|
||||
<?php
|
||||
$sql = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE EXISTS (SELECT 1 FROM trips WHERE trip_user_id = user_id) ORDER BY user_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$option_id = intval($row['user_id']); ?>
|
||||
<option <?= $driver_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionUser($option_id, $row['user_name'], $row['user_avatar'] ?? '') ?>><?= escapeHtml($row['user_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (!$client_id) { ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<select class="form-select select2" name="client">
|
||||
<option value="">- Any client -</option>
|
||||
<?php
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE EXISTS (SELECT 1 FROM trips WHERE trip_client_id = client_id) " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$option_id = intval($row['client_id']); ?>
|
||||
<option <?= $client_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Showing</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-filter"></i></span>
|
||||
<select class="form-select select2" name="show">
|
||||
<?php foreach ($shows as $key => $s) { ?>
|
||||
<option <?= $show_filter === $key ? 'selected' : '' ?> value="<?= escapeHtml($key) ?>"><?= escapeHtml($key === '' ? 'All trips' : $s['label']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Dated To</label>
|
||||
<div class="input-group">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Dated from</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="dtt" value="<?= $date_to_filter ?>" max="2999-12-31">
|
||||
<input type="date" class="form-control" name="dtf" value="<?= $date_from_filter ?>" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Dated to</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="dtt" value="<?= $date_to_filter ?>" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
344
agent/modals/trip/trip_form.php
Normal file
344
agent/modals/trip/trip_form.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The fields of a trip - shared by New Trip, Edit, Copy and Return Trip.
|
||||
* Set before including:
|
||||
*
|
||||
* $form_mode new | edit | copy | return
|
||||
* $form_row tripRow() for edit, copy and return; null for new
|
||||
* $form_client_id the client a new one starts with (0 for none)
|
||||
* $form_location_id the site a new one goes to (0 for none)
|
||||
* $form_ticket_id the ticket a new one is for (0 for none)
|
||||
*
|
||||
* Posts date, driver, client_id, ticket_id, location_id, source,
|
||||
* destination, miles (one way), round_trip, odometer_start, odometer_end
|
||||
* and purpose. Picking a client offers their sites and tickets; a site
|
||||
* fills in where to, a ticket the purpose and its site; a route driven
|
||||
* before fills in the miles; the total follows the miles and round trip,
|
||||
* and the odometer readings make the miles when both are there.
|
||||
*
|
||||
* Should not be accessed directly.
|
||||
*/
|
||||
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
|
||||
$form_edit = $form_mode === 'edit';
|
||||
$form_return = $form_mode === 'return';
|
||||
$form_id = 'tripForm' . ucfirst($form_mode) . ($form_row ? intval($form_row['trip_id']) : '');
|
||||
|
||||
$form_client = $form_row ? intval($form_row['trip_client_id']) : intval($form_client_id ?? 0);
|
||||
$form_location = $form_row ? intval($form_row['trip_location_id']) : intval($form_location_id ?? 0);
|
||||
$form_ticket = $form_row ? intval($form_row['trip_ticket_id']) : intval($form_ticket_id ?? 0);
|
||||
$form_driver = $form_row && $form_edit ? intval($form_row['trip_user_id']) : intval($session_user_id);
|
||||
$form_date = $form_row && ($form_edit || $form_return) ? (string) $form_row['trip_date'] : date('Y-m-d');
|
||||
$form_source = $form_row ? (string) $form_row['trip_source'] : (tripLastStart() ?: tripOfficeAddress());
|
||||
$form_destination = $form_row ? (string) $form_row['trip_destination'] : '';
|
||||
$form_round = $form_row ? intval($form_row['round_trip']) === 1 : true;
|
||||
$form_miles = $form_row ? tripOneWay($form_row) : null;
|
||||
$form_purpose = $form_row ? (string) $form_row['trip_purpose'] : (string) ($form_purpose ?? '');
|
||||
$form_odo_start = $form_row && $form_edit ? $form_row['trip_start_odometer'] : null;
|
||||
$form_odo_end = $form_row && $form_edit ? $form_row['trip_end_odometer'] : null;
|
||||
|
||||
// The way back: from where it went to where it started, one way
|
||||
if ($form_return) {
|
||||
[$form_source, $form_destination] = [$form_destination, $form_source];
|
||||
$form_location = 0;
|
||||
$form_round = false;
|
||||
$form_purpose = mb_substr('Back from ' . $form_row['trip_destination'] . ($form_purpose !== '' ? " - $form_purpose" : ''), 0, 200);
|
||||
}
|
||||
|
||||
$form_can_manage = tripCanManage();
|
||||
$form_drivers = tripDrivers($form_row ? $form_row['trip_user_id'] : 0);
|
||||
if (!$form_can_manage) {
|
||||
$form_drivers = array_intersect_key($form_drivers, array_flip(array_filter([intval($session_user_id), $form_edit ? intval($form_row['trip_user_id']) : 0])));
|
||||
}
|
||||
$form_clients = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients
|
||||
WHERE (client_archived_at IS NULL OR client_id = $form_client) " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$form_clients[] = $row;
|
||||
}
|
||||
$form_sites = tripClientSites(0, $form_location);
|
||||
$form_tickets = tripClientTickets(0, $form_ticket);
|
||||
$form_routes = tripKnownRoutes();
|
||||
$form_places = tripStartPlaces();
|
||||
$unit = tripUnit();
|
||||
|
||||
?>
|
||||
<div class="row g-3" id="<?= $form_id ?>" data-trip-form="<?= $form_mode ?>">
|
||||
|
||||
<?php if ($form_return) { ?>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info small mb-0"><i class="fas fa-undo-alt me-2"></i>The way back from <?= escapeHtml($form_row['trip_destination']) ?> - a trip of its own, one way.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Date <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
||||
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= escapeHtml($form_date) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Driver <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="driver" required>
|
||||
<?php foreach ($form_drivers as $driver_id => $driver) { ?>
|
||||
<option value="<?= $driver_id ?>" <?= $driver_id === $form_driver ? 'selected' : '' ?><?= selectOptionUser($driver_id, $driver['user_name'], $driver['user_avatar'] ?? '') ?>><?= escapeHtml($driver['user_name']) ?><?= $driver_id === intval($session_user_id) ? ' (me)' : '' ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if (!$form_can_manage) { ?>
|
||||
<div class="form-text">You log your own trips.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<select class="form-select select2" name="client_id" data-trip-client>
|
||||
<option value="0">- Our own - not for a client -</option>
|
||||
<?php foreach ($form_clients as $client) { ?>
|
||||
<option value="<?= intval($client['client_id']) ?>" <?= $form_client === intval($client['client_id']) ? 'selected' : '' ?><?= selectOptionClient($client['client_id'], $client['client_name'], $client['client_abbreviation'] ?? '') ?>><?= escapeHtml($client['client_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" data-trip-ticket-box hidden>
|
||||
<label class="form-label">For the ticket</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-life-ring"></i></span>
|
||||
<select class="form-select" name="ticket_id" data-trip-ticket data-selected="<?= $form_ticket ?>">
|
||||
<option value="0">- No ticket -</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">From <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-dot-circle"></i></span>
|
||||
<input type="text" class="form-control" name="source" maxlength="200" value="<?= escapeHtml($form_source) ?>" list="<?= $form_id ?>Places" placeholder="Where you started - the office, home" required data-trip-from>
|
||||
<datalist id="<?= $form_id ?>Places">
|
||||
<?php foreach ($form_places as $place) { ?>
|
||||
<option value="<?= escapeHtml($place) ?>"></option>
|
||||
<?php } ?>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" data-trip-site-box hidden>
|
||||
<label class="form-label">Their site</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-marker-alt"></i></span>
|
||||
<select class="form-select" name="location_id" data-trip-site data-selected="<?= $form_location ?>">
|
||||
<option value="0">- Somewhere else -</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">To <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-map-marker-alt text-danger"></i></span>
|
||||
<input type="text" class="form-control" name="destination" maxlength="200" value="<?= escapeHtml($form_destination) ?>" placeholder="The address, or the place" required data-trip-to>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= ucfirst($unit) ?> one way <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-road"></i></span>
|
||||
<input type="text" class="form-control text-end" name="miles" inputmode="decimal" placeholder="0.0" value="<?= $form_miles === null ? '' : number_format($form_miles, 1, '.', '') ?>" required data-trip-miles <?= $form_mode === 'new' && !$form_location ? 'autofocus' : '' ?>>
|
||||
<span class="input-group-text"><?= tripUnit(true) ?></span>
|
||||
</div>
|
||||
<div class="form-text" data-trip-known hidden><i class="fas fa-history me-1"></i>As the last time you drove it</div>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-md-end">
|
||||
<div class="form-check form-switch mb-md-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="round_trip" value="1" id="<?= $form_id ?>Round" <?= $form_round ? 'checked' : '' ?> data-trip-round>
|
||||
<label class="form-check-label" for="<?= $form_id ?>Round">Round trip - there and back</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-md-end">
|
||||
<div class="mb-md-2"><span class="text-body-secondary">The trip is</span> <strong class="fs-5" data-trip-total></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<a class="small" data-bs-toggle="collapse" href="#<?= $form_id ?>Odometer" role="button"><i class="fas fa-tachometer-alt me-1"></i>Odometer readings</a>
|
||||
<div class="collapse <?= $form_odo_start !== null || $form_odo_end !== null ? 'show' : '' ?>" id="<?= $form_id ?>Odometer">
|
||||
<div class="row g-3 mt-0">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">At the start</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tachometer-alt"></i></span>
|
||||
<input type="text" class="form-control text-end" name="odometer_start" inputmode="numeric" value="<?= $form_odo_start === null ? '' : intval($form_odo_start) ?>" data-trip-odo-start>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">At the end</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-flag-checkered"></i></span>
|
||||
<input type="text" class="form-control text-end" name="odometer_end" inputmode="numeric" value="<?= $form_odo_end === null ? '' : intval($form_odo_end) ?>" data-trip-odo-end>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 form-text mt-1">With both, the trip is what the odometer says.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">What for <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text align-items-start pt-2"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<textarea class="form-control" rows="2" name="purpose" maxlength="200" placeholder="Onsite - replace the front office switch" required data-trip-purpose><?= escapeHtml($form_purpose) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var root = document.getElementById(<?= json_encode($form_id) ?>);
|
||||
if (!root || root.dataset.wired) {
|
||||
return;
|
||||
}
|
||||
root.dataset.wired = '1';
|
||||
var sites = <?= json_encode((object) $form_sites) ?>;
|
||||
var tickets = <?= json_encode((object) $form_tickets) ?>;
|
||||
var routes = <?= json_encode((object) $form_routes) ?>;
|
||||
var unit = <?= json_encode(tripUnit(true)) ?>;
|
||||
var client = root.querySelector('[data-trip-client]');
|
||||
var ticket = root.querySelector('[data-trip-ticket]');
|
||||
var site = root.querySelector('[data-trip-site]');
|
||||
var from = root.querySelector('[data-trip-from]');
|
||||
var to = root.querySelector('[data-trip-to]');
|
||||
var miles = root.querySelector('[data-trip-miles]');
|
||||
var round = root.querySelector('[data-trip-round]');
|
||||
var total = root.querySelector('[data-trip-total]');
|
||||
var known = root.querySelector('[data-trip-known]');
|
||||
var odoStart = root.querySelector('[data-trip-odo-start]');
|
||||
var odoEnd = root.querySelector('[data-trip-odo-end]');
|
||||
var purpose = root.querySelector('[data-trip-purpose]');
|
||||
var autoTo = '';
|
||||
var autoMiles = false;
|
||||
|
||||
function number(value) {
|
||||
var n = Number(String(value || '').replace(/[\s,]/g, ''));
|
||||
return isNaN(n) ? null : n;
|
||||
}
|
||||
|
||||
function fill(select, list, selected, none) {
|
||||
select.innerHTML = '';
|
||||
var first = document.createElement('option');
|
||||
first.value = '0';
|
||||
first.textContent = none;
|
||||
select.appendChild(first);
|
||||
list.forEach(function (item) {
|
||||
var option = document.createElement('option');
|
||||
option.value = item[0];
|
||||
option.textContent = item[1] + (item.length > 2 && typeof item[2] === 'string' && item[2] !== item[1] ? ' - ' + item[2] : '');
|
||||
option.selected = String(item[0]) === String(selected);
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// A client's sites and tickets
|
||||
function clientChanged(first) {
|
||||
var id = client.value;
|
||||
var siteList = sites[id] || [];
|
||||
var ticketList = tickets[id] || [];
|
||||
fill(site, siteList, first ? site.getAttribute('data-selected') : '0', '- Somewhere else -');
|
||||
fill(ticket, ticketList, first ? ticket.getAttribute('data-selected') : '0', '- No ticket -');
|
||||
root.querySelector('[data-trip-site-box]').hidden = !siteList.length;
|
||||
root.querySelector('[data-trip-ticket-box]').hidden = !ticketList.length;
|
||||
if (!first && autoTo !== '' && to.value === autoTo) {
|
||||
to.value = '';
|
||||
autoTo = '';
|
||||
}
|
||||
}
|
||||
|
||||
// A site fills in where to, unless it was typed
|
||||
function siteChanged() {
|
||||
var found = (sites[client.value] || []).filter(function (s) { return String(s[0]) === site.value; })[0];
|
||||
if (found && (to.value === '' || to.value === autoTo)) {
|
||||
to.value = found[2];
|
||||
autoTo = found[2];
|
||||
}
|
||||
routeChanged();
|
||||
}
|
||||
|
||||
// A ticket fills in the purpose, and its site when none is picked
|
||||
function ticketChanged() {
|
||||
var found = (tickets[client.value] || []).filter(function (t) { return String(t[0]) === ticket.value; })[0];
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
if (purpose.value.trim() === '') {
|
||||
purpose.value = 'Onsite - ' + found[1].replace(/^[^ ]+ - /, '');
|
||||
}
|
||||
if (found[2] && site.value === '0' && site.querySelector('option[value="' + found[2] + '"]')) {
|
||||
site.value = String(found[2]);
|
||||
siteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// A route driven before fills in the miles, unless they were typed
|
||||
function routeChanged() {
|
||||
var key = from.value.trim().toLowerCase() + '|' + to.value.trim().toLowerCase();
|
||||
var value = routes[key];
|
||||
if (value !== undefined && (miles.value.trim() === '' || autoMiles) && !odometer()) {
|
||||
miles.value = Number(value).toFixed(1);
|
||||
autoMiles = true;
|
||||
}
|
||||
known.hidden = !(autoMiles && value !== undefined && number(miles.value) === Number(value));
|
||||
update();
|
||||
}
|
||||
|
||||
function odometer() {
|
||||
var a = number(odoStart.value);
|
||||
var b = number(odoEnd.value);
|
||||
return odoStart.value.trim() !== '' && odoEnd.value.trim() !== '' && a !== null && b !== null && b >= a ? b - a : null;
|
||||
}
|
||||
|
||||
function update() {
|
||||
var odo = odometer();
|
||||
if (odo !== null) {
|
||||
miles.value = (odo / (round.checked ? 2 : 1)).toFixed(1);
|
||||
}
|
||||
miles.readOnly = odo !== null;
|
||||
var m = number(miles.value);
|
||||
total.textContent = m === null ? '-' : (Math.round((odo !== null ? odo : m * (round.checked ? 2 : 1)) * 10) / 10).toFixed(1) + ' ' + unit;
|
||||
}
|
||||
|
||||
client.addEventListener('change', function () { clientChanged(false); });
|
||||
site.addEventListener('change', siteChanged);
|
||||
ticket.addEventListener('change', ticketChanged);
|
||||
[from, to].forEach(function (el) {
|
||||
el.addEventListener('change', routeChanged);
|
||||
});
|
||||
miles.addEventListener('input', function () {
|
||||
autoMiles = false;
|
||||
known.hidden = true;
|
||||
update();
|
||||
});
|
||||
[round, odoStart, odoEnd].forEach(function (el) {
|
||||
el.addEventListener('input', update);
|
||||
el.addEventListener('change', update);
|
||||
});
|
||||
|
||||
clientChanged(true);
|
||||
if (site.value !== '0' && to.value === '') {
|
||||
siteChanged();
|
||||
}
|
||||
if (ticket.value !== '0' && purpose.value.trim() === '') {
|
||||
ticketChanged();
|
||||
}
|
||||
routeChanged();
|
||||
})();
|
||||
</script>
|
||||
@@ -61,6 +61,8 @@ function ticketMergeInto($child, $parent, $comment, $move_replies = false) {
|
||||
|
||||
if ($move_replies) {
|
||||
mysqli_query($mysqli, "UPDATE ticket_replies SET ticket_reply_ticket_id = $parent_id WHERE ticket_reply_ticket_id = $ticket_id");
|
||||
// The work goes over, and the driving for it with it
|
||||
mysqli_query($mysqli, "UPDATE trips SET trip_ticket_id = $parent_id WHERE trip_ticket_id = $ticket_id");
|
||||
}
|
||||
if (empty($child['ticket_first_response_at'])) {
|
||||
setTicketFirstResponse($ticket_id);
|
||||
@@ -1294,6 +1296,8 @@ if (isset($_POST['change_client_ticket'])) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_client_id = $client_id, ticket_contact_id = $contact_id, ticket_asset_id = 0, ticket_location_id = 0,
|
||||
ticket_vendor_id = $vendor_id, ticket_project_id = $project_id WHERE ticket_id = $ticket_id LIMIT 1");
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_assets WHERE ticket_id = $ticket_id");
|
||||
// The driving for it goes with it - not to the old client's site
|
||||
mysqli_query($mysqli, "UPDATE trips SET trip_client_id = $client_id, trip_location_id = 0 WHERE trip_ticket_id = $ticket_id");
|
||||
applyTicketSla($ticket_id);
|
||||
|
||||
$old_client_name = $old_client_id ? (string) getFieldById('clients', $old_client_id, 'client_name') : 'no client';
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - GET/POST request handler for trips (accounting related)
|
||||
* ITFlow - trips (functions/trips.php does the work).
|
||||
*
|
||||
* New, Edit, Copy and Return Trip all post through tripSave(). Anyone logs
|
||||
* and changes their own trips; someone else's needs write access to
|
||||
* Financial, deleting them full access. Bulk Set Driver and Set Client need
|
||||
* write access, bulk delete full access.
|
||||
*/
|
||||
|
||||
// Todo - JQ 2026-03-02 - Possibly need another Perm for trips
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['add_trip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
require_once 'trip_model.php';
|
||||
|
||||
$client_id = intval($_POST['client_id']);
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
$trip_id = tripSave($_POST, 0, $error);
|
||||
if (!$trip_id) {
|
||||
flashAlert(escapeHtml($error), 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO trips SET trip_date = '$date', trip_source = '$source', trip_destination = '$destination', trip_miles = $miles, round_trip = $roundtrip, trip_purpose = '$purpose', trip_user_id = $user_id, trip_client_id = $client_id");
|
||||
$row = tripRow($trip_id, false);
|
||||
logAudit("Trip", "Create", "$session_name logged the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
|
||||
|
||||
$trip_id = mysqli_insert_id($mysqli);
|
||||
|
||||
logAudit("Trip", "Create", "$session_name logged trip from $source to $destination", $client_id , $trip_id);
|
||||
|
||||
flashAlert("Trip from <strong>$source</strong> to <strong>$destination</strong> logged");
|
||||
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> logged - " . escapeHtml(tripMilesText($row['trip_miles'])));
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -36,27 +34,16 @@ if (isset($_POST['edit_trip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
require_once 'trip_model.php';
|
||||
|
||||
$trip_id = intval($_POST['trip_id']);
|
||||
|
||||
// Check the client the trip belongs to now, not just the one submitted
|
||||
$existing_client_id = intval(getFieldById('trips', $trip_id, 'trip_client_id'));
|
||||
if ($existing_client_id) {
|
||||
enforceClientAccess($existing_client_id);
|
||||
$trip_id = intval($_POST['trip_id'] ?? 0);
|
||||
if (!tripSave($_POST, $trip_id, $error)) {
|
||||
flashAlert(escapeHtml($error), 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$client_id = intval($_POST['client_id']);
|
||||
$row = tripRow($trip_id, false);
|
||||
logAudit("Trip", "Edit", "$session_name edited the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli,"UPDATE trips SET trip_date = '$date', trip_source = '$source', trip_destination = '$destination', trip_miles = $miles, trip_purpose = '$purpose', round_trip = $roundtrip, trip_user_id = $user_id, trip_client_id = $client_id WHERE trip_id = $trip_id");
|
||||
|
||||
logAudit("Trip", "Edit", "$session_name edited trip", $client_id , $trip_id);
|
||||
|
||||
flashAlert("Trip edited");
|
||||
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> saved");
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -66,25 +53,89 @@ if (isset($_GET['delete_trip'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_financial', 3);
|
||||
|
||||
$trip_id = intval($_GET['delete_trip']);
|
||||
|
||||
// Get Trip Info and Client ID for logging
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT trip_client_id, trip_destination, trip_source FROM trips WHERE trip_id = $trip_id"));
|
||||
$client_id = intval($row['trip_client_id']);
|
||||
$trip_source = escapeSql($row['trip_source']);
|
||||
$trip_destination = escapeSql($row['trip_destination']);
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
$row = tripDelete(intval($_GET['delete_trip']), $error);
|
||||
if (!$row) {
|
||||
flashAlert(escapeHtml($error), 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli,"DELETE FROM trips WHERE trip_id = $trip_id");
|
||||
logAudit("Trip", "Delete", "$session_name deleted the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']));
|
||||
|
||||
logAudit("Trip", "Delete", "$session_name deleted trip ($trip_source - $trip_destination)", $client_id);
|
||||
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> deleted", 'error');
|
||||
|
||||
flashAlert("Trip ($trip_source - $trip_destination) deleted", 'error');
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
/* Set the driver (an agent) or client (0 for none - the site and ticket go with the old one) of the ticked trips. */
|
||||
if (isset($_POST['bulk_edit_trips'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_financial', 2);
|
||||
|
||||
$field = (string) ($_POST['field'] ?? '');
|
||||
$value = intval($_POST['value'] ?? 0);
|
||||
$set = '';
|
||||
$label = '';
|
||||
if ($field === 'driver') {
|
||||
$drivers = tripDrivers();
|
||||
if (isset($drivers[$value])) {
|
||||
$set = "trip_user_id = $value";
|
||||
$label = 'the driver to ' . $drivers[$value]['user_name'];
|
||||
}
|
||||
} elseif ($field === 'client') {
|
||||
if (!$value) {
|
||||
$set = 'trip_client_id = 0, trip_location_id = 0, trip_ticket_id = 0';
|
||||
$label = 'no client';
|
||||
} elseif (mysqli_num_rows(mysqli_query($mysqli, "SELECT client_id FROM clients WHERE client_id = $value " . clientScopeSql('client_id')))) {
|
||||
// The site and ticket first - the client after, as MySQL sets them in order
|
||||
$set = "trip_location_id = IF(trip_client_id = $value, trip_location_id, 0), trip_ticket_id = IF(trip_client_id = $value, trip_ticket_id, 0), trip_client_id = $value";
|
||||
$label = 'the client to ' . getFieldById('clients', $value, 'client_name');
|
||||
}
|
||||
}
|
||||
$trip_ids = array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? []))));
|
||||
if ($set === '' || !$trip_ids) {
|
||||
flashAlert($trip_ids ? 'Pick what to set them to' : 'Tick the trips to change first', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($trip_ids as $trip_id) {
|
||||
$row = tripRow($trip_id);
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
mysqli_query($mysqli, "UPDATE trips SET $set WHERE trip_id = $trip_id");
|
||||
logAudit("Trip", "Edit", "$session_name set " . escapeSql($label) . " on the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
|
||||
$count++;
|
||||
}
|
||||
logAudit("Trip", "Bulk Edit", "$session_name set " . escapeSql($label) . " on $count trip" . ($count === 1 ? '' : 's'));
|
||||
|
||||
flashAlert("Set " . escapeHtml($label) . " on <strong>$count</strong> trip" . ($count === 1 ? '' : 's'));
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_delete_trips'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_financial', 3);
|
||||
|
||||
$count = 0;
|
||||
foreach (array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? [])))) as $trip_id) {
|
||||
$row = tripDelete($trip_id);
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
logAudit("Trip", "Delete", "$session_name deleted the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']));
|
||||
$count++;
|
||||
}
|
||||
logAudit("Trip", "Bulk Delete", "$session_name deleted $count trip" . ($count === 1 ? '' : 's'));
|
||||
|
||||
flashAlert("<strong>$count</strong> trip" . ($count === 1 ? '' : 's') . " deleted", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -94,54 +145,23 @@ if (isExportRequest('export_trips')) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
// Exports are reads - see CONTRIBUTING.md
|
||||
enforceUserPermission('module_financial');
|
||||
|
||||
$format = resolveExportFormat($_POST['export_trips']);
|
||||
|
||||
// Filters inherited from the trips page - mirrors agent/trips.php
|
||||
$filter_summary = [];
|
||||
|
||||
// The same filters as the trips list, over the trips this agent may see
|
||||
$src = $_POST;
|
||||
if (!empty($_POST['client_id'])) {
|
||||
$client_id = intval($_POST['client_id']);
|
||||
$client_query = "AND trip_client_id = $client_id";
|
||||
$client_name = getFieldById('clients', $client_id, 'client_name');
|
||||
$file_name_prepend = "$client_name-";
|
||||
$filter_summary['Client'] = $client_name;
|
||||
|
||||
$src['client'] = intval($_POST['client_id']);
|
||||
}
|
||||
$where = tripFiltersSql($src, $filter_summary);
|
||||
$client_id = intval($src['client'] ?? 0);
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
} else {
|
||||
$client_query = '';
|
||||
$client_id = 0; // for Logging
|
||||
$file_name_prepend = "$session_company_name-";
|
||||
}
|
||||
|
||||
// Date Filter
|
||||
$dtf = escapeSql(!empty($_POST['dtf']) ? $_POST['dtf'] : '1970-01-01');
|
||||
$dtt = escapeSql(!empty($_POST['dtt']) ? $_POST['dtt'] : '2099-12-31');
|
||||
$date_range = formatExportDateRange($dtf, $dtt);
|
||||
if ($date_range) {
|
||||
$filter_summary['Dated'] = $date_range;
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
$q = escapeSql($_POST['q'] ?? '');
|
||||
if (!empty($q)) {
|
||||
$filter_summary['Search'] = $_POST['q'];
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT * FROM trips
|
||||
LEFT JOIN clients ON trip_client_id = client_id
|
||||
LEFT JOIN users ON trip_user_id = user_id
|
||||
WHERE (trip_purpose LIKE '%$q%' OR trip_source LIKE '%$q%' OR trip_destination LIKE '%$q%' OR trip_miles LIKE '%$q%' OR client_name LIKE '%$q%' OR user_name LIKE '%$q%')
|
||||
AND DATE(trip_date) BETWEEN '$dtf' AND '$dtt'
|
||||
AND trip_archived_at IS NULL
|
||||
$client_query
|
||||
" . clientScopeSql('trip_client_id') . "
|
||||
ORDER BY trip_date ASC"
|
||||
);
|
||||
$sql = mysqli_query($mysqli, "SELECT trips.*, client_name, driver.user_name AS driver_name, location_name, ticket_prefix, ticket_number, ticket_subject
|
||||
FROM trips " . tripJoinsSql() . "
|
||||
WHERE $where
|
||||
ORDER BY trips.trip_date ASC, trips.trip_id ASC");
|
||||
|
||||
$num_rows = mysqli_num_rows($sql);
|
||||
|
||||
@@ -149,9 +169,13 @@ if (isExportRequest('export_trips')) {
|
||||
|
||||
guardExportPdfRowCount($format, $num_rows);
|
||||
|
||||
$export = beginExport('trips', $format, $file_name_prepend . 'Trips', 'Trips', summarizeExportFilters($filter_summary));
|
||||
$export = beginExport('trips', $format, ($client_id ? getFieldById('clients', $client_id, 'client_name') : $session_company_name) . '-Trips', 'Trips', summarizeExportFilters($filter_summary));
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$row['user_name'] = $row['driver_name'];
|
||||
$row['round_trip_display'] = intval($row['round_trip']) ? 'Yes' : 'No';
|
||||
$row['trip_one_way'] = number_format(tripOneWay($row), 1, '.', '');
|
||||
$row['ticket_display'] = $row['ticket_number'] !== null ? ticketReference($row) . ' - ' . $row['ticket_subject'] : '';
|
||||
addExportRow($export, $row);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
$date = escapeSql($_POST['date']);
|
||||
$source = escapeSql($_POST['source']);
|
||||
$destination = escapeSql($_POST['destination']);
|
||||
$miles = floatval($_POST['miles']);
|
||||
$roundtrip = intval($_POST['roundtrip'] ?? 0);
|
||||
$purpose = escapeSql($_POST['purpose']);
|
||||
$user_id = intval($_POST['user']);
|
||||
@@ -4,7 +4,9 @@
|
||||
* Mileage - the trips logged in the period: miles by agent and by client,
|
||||
* every trip listed, and what it comes to at a rate per mile you enter (your
|
||||
* reimbursement or tax-deduction rate - nothing is assumed). Miles count as
|
||||
* entered on each trip, the same as the Trips list and the dashboard.
|
||||
* entered on each trip (the whole trip - both ways for a round trip), the
|
||||
* same as the Trips list and the dashboard. A trip's date opens it on the
|
||||
* Trips list; its ticket and site show under the purpose.
|
||||
*/
|
||||
|
||||
require_once "includes/inc_report.php";
|
||||
@@ -21,10 +23,13 @@ $where = "trip_archived_at IS NULL AND " . reportRangeSql('trip_date', $period)
|
||||
|
||||
$trips = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT trip_id, trip_date, trip_purpose, trip_source, trip_destination, trip_miles, round_trip,
|
||||
trip_user_id, user_name, user_avatar, trip_client_id, client_name, client_abbreviation
|
||||
trip_user_id, user_name, user_avatar, trip_client_id, client_name, client_abbreviation,
|
||||
trip_location_id, location_name, trip_ticket_id, ticket_prefix, ticket_number
|
||||
FROM trips
|
||||
LEFT JOIN users ON trip_user_id = user_id
|
||||
LEFT JOIN clients ON trip_client_id = client_id
|
||||
LEFT JOIN locations ON location_id = trip_location_id
|
||||
LEFT JOIN tickets ON ticket_id = trip_ticket_id
|
||||
WHERE $where
|
||||
ORDER BY trip_date DESC, trip_id DESC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
@@ -64,14 +69,27 @@ usort($by_client, function ($a, $b) { return $b['miles'] <=> $a['miles']; });
|
||||
$value_column = ['label' => 'At ' . ($rate ? reportMoney($rate) . ' a mile' : 'your rate'), 'type' => 'money', 'csv_label' => 'Amount'];
|
||||
|
||||
$trip_columns = [
|
||||
'trip_date' => ['label' => 'Date', 'type' => 'date', 'class' => 'text-nowrap'],
|
||||
'trip_date' => ['label' => 'Date', 'type' => 'date', 'class' => 'text-nowrap', 'html' => function ($row) {
|
||||
// Opens the trip's quick look on the Trips list
|
||||
return '<a href="/agent/trips.php?open=' . intval($row['trip_id']) . '">' . escapeHtml(date('M j, Y', strtotime($row['trip_date']))) . '</a>';
|
||||
}],
|
||||
'user_name' => ['label' => 'Agent', 'html' => function ($row) {
|
||||
return reportAgentCell($row['trip_user_id'], $row['user_name'] ?? '', $row['user_avatar'] ?? '', 'No one');
|
||||
}],
|
||||
'client_name' => ['label' => 'Client', 'html' => function ($row) {
|
||||
return reportClientCell($row['trip_client_id'], $row['client_name'] ?? '', $row['client_abbreviation'] ?? '');
|
||||
}],
|
||||
'trip_purpose' => ['label' => 'Purpose', 'class' => 'itflow-report-text'],
|
||||
'trip_purpose' => ['label' => 'Purpose', 'class' => 'itflow-report-text', 'html' => function ($row) {
|
||||
$for = [];
|
||||
if ($row['ticket_number'] !== null && intval($row['trip_ticket_id'])) {
|
||||
$for[] = '<span class="font-monospace">' . escapeHtml(ticketReference($row)) . '</span>';
|
||||
}
|
||||
if ($row['location_name'] !== null && intval($row['trip_location_id'])) {
|
||||
$for[] = '<i class="fas fa-map-marker-alt me-1"></i>' . escapeHtml($row['location_name']);
|
||||
}
|
||||
return escapeHtml($row['trip_purpose']) . ($for ? '<div class="small text-body-secondary">' . implode(' · ', $for) . '</div>' : '');
|
||||
}],
|
||||
|
||||
'route' => ['label' => 'From → to', 'class' => 'small text-body-secondary', 'csv' => function ($row) {
|
||||
return $row['trip_source'] . ' → ' . $row['trip_destination'];
|
||||
}, 'html' => function ($row) {
|
||||
@@ -87,6 +105,15 @@ $trip_columns = [
|
||||
if ($rate) {
|
||||
$trip_columns['value'] = $value_column;
|
||||
}
|
||||
// The CSV has the site and ticket in columns of their own
|
||||
$trip_csv_columns = array_slice($trip_columns, 0, 4, true) + [
|
||||
'location_name' => ['label' => 'Site', 'csv' => function ($row) {
|
||||
return intval($row['trip_location_id']) ? (string) $row['location_name'] : '';
|
||||
}],
|
||||
'ticket' => ['label' => 'Ticket', 'csv' => function ($row) {
|
||||
return $row['ticket_number'] !== null && intval($row['trip_ticket_id']) ? ticketReference($row) : '';
|
||||
}],
|
||||
] + array_slice($trip_columns, 4, null, true);
|
||||
|
||||
$agent_columns = [
|
||||
'agent' => ['label' => 'Agent', 'html' => function ($row) {
|
||||
@@ -116,7 +143,7 @@ if (reportExporting('clients')) {
|
||||
reportCsv("$name by client", $client_columns, $by_client, ['client' => 'Total'] + $totals);
|
||||
}
|
||||
if (reportExporting()) {
|
||||
reportCsv($name, $trip_columns, $trips, ['trip_date' => 'Total', 'miles' => $miles, 'value' => $totals['value']]);
|
||||
reportCsv($name, $trip_csv_columns, $trips, ['trip_date' => 'Total', 'miles' => $miles, 'value' => $totals['value']]);
|
||||
}
|
||||
|
||||
$buckets = reportBuckets($period);
|
||||
|
||||
@@ -458,6 +458,11 @@ if (isset($_GET['ticket_id'])) {
|
||||
$show_quote_button = $can_edit_ticket && $config_module_enable_accounting && $ticket_billable == 1 && empty($quote_id) && empty($invoice_id) && lookupUserPermission("module_sales") >= 2;
|
||||
$show_invoice_button = $can_edit_ticket && $config_module_enable_accounting && $ticket_billable == 1 && empty($invoice_id) && lookupUserPermission("module_sales") >= 2;
|
||||
|
||||
// Trips - the driving done for it: anyone working it can log theirs (functions/trips.php)
|
||||
$show_trips = $config_module_enable_accounting && $client_id > 0;
|
||||
$ticket_trips = $show_trips ? tripsFor('trip_ticket_id', $ticket_id, 5) : [];
|
||||
[$ticket_trip_count, $ticket_trip_miles] = $show_trips ? tripTotalsFor('trip_ticket_id', $ticket_id) : [0, 0.0];
|
||||
|
||||
// Values in the Details panel are links only when they can actually be changed
|
||||
$can_change_fields = $can_edit_ticket && !$ticket_is_closed;
|
||||
|
||||
@@ -519,7 +524,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
the rarer actions live in the menu; Invoice comes out of it once
|
||||
the work is done, which is when it is actually wanted.
|
||||
-->
|
||||
<?php if ($can_edit_ticket || $ai_summary_available) { ?>
|
||||
<?php if ($can_edit_ticket || $ai_summary_available || $show_trips) { ?>
|
||||
<div class="ticket-actions d-flex flex-wrap align-items-center gap-2 d-print-none">
|
||||
|
||||
<?php if ($can_edit_ticket && $ticket_is_open) { ?>
|
||||
@@ -572,6 +577,14 @@ if (isset($_GET['ticket_id'])) {
|
||||
<i class="fas fa-fw fa-magic me-2"></i>Summarize with AI
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($show_trips) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/trip/trip_add.php?ticket_id=<?= $ticket_id ?>" data-modal-size="lg">
|
||||
<i class="fas fa-fw fa-route me-2"></i>Log a trip
|
||||
</a>
|
||||
<?php if ($can_edit_ticket) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php if ($can_edit_ticket) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket/ticket_merge.php?ticket_id=<?= $ticket_id ?>">
|
||||
<i class="fas fa-fw fa-clone me-2"></i>Merge into another ticket
|
||||
@@ -1229,6 +1242,43 @@ if (isset($_GET['ticket_id'])) {
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Trips - the driving done for it, for an onsite ticket or once there is some -->
|
||||
<?php if ($show_trips && ($ticket_trip_count || $ticket_onsite)) { ?>
|
||||
<div class="card mb-3 itflow-ticket-trips">
|
||||
<div class="card-header px-3 py-2">
|
||||
<h5 class="card-title mt-1"><i class="fas fa-fw fa-route me-2"></i>Trips<?php if ($ticket_trip_count) { ?> <span class="badge text-bg-secondary ms-1"><?= $ticket_trip_count ?></span><?php } ?></h5>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-tool ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip_add.php?ticket_id=<?= $ticket_id ?>" title="Log a trip"><i class="fas fa-plus"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($ticket_trips) { ?>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($ticket_trips as $trip) { ?>
|
||||
<a href="#" class="list-group-item list-group-item-action px-3 py-2 ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip.php?id=<?= intval($trip['trip_id']) ?>">
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<span class="small text-truncate"><?= escapeHtml(date('M j', strtotime($trip['trip_date']))) ?> · <?= escapeHtml($trip['driver_name'] ?? 'Unknown') ?></span>
|
||||
<span class="small fw-semibold text-nowrap"><?= escapeHtml(tripMilesText($trip['trip_miles'])) ?><?php if (intval($trip['round_trip'])) { ?><i class="fas fa-sync-alt text-body-secondary ms-1" title="Round trip"></i><?php } ?></span>
|
||||
</div>
|
||||
<div class="small text-body-secondary text-truncate"><?= escapeHtml($trip['trip_source']) ?> → <?= escapeHtml($trip['trip_destination']) ?></div>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="card-footer px-3 py-2 small d-flex justify-content-between">
|
||||
<span class="text-body-secondary"><?= escapeHtml(tripMilesText($ticket_trip_miles)) ?> driven</span>
|
||||
<?php if ($ticket_trip_count > count($ticket_trips)) { ?>
|
||||
<a href="trips.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>">All <?= $ticket_trip_count ?></a>
|
||||
<?php } else { ?>
|
||||
<a href="trips.php?client_id=<?= $client_id ?>&ticket_id=<?= $ticket_id ?>">Open in Trips</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="card-body p-3 small text-body-secondary">
|
||||
An onsite ticket - drove out for it? <a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip_add.php?ticket_id=<?= $ticket_id ?>">Log the trip</a> to keep the miles with the ticket.
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Tasks - the checklist, with the one place the progress count is shown -->
|
||||
<?php if (!$ticket_is_resolved || $task_count) { ?>
|
||||
<div class="card mb-3">
|
||||
|
||||
539
agent/trips.php
539
agent/trips.php
@@ -1,5 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Trips - the driving the business does, for clients and for itself.
|
||||
*
|
||||
* Along the top: the miles this month against the same days of last month,
|
||||
* the year so far, your own this month, and how much of the year's driving
|
||||
* was for clients. Pills cut the list to yours, for a client, for a ticket
|
||||
* or our own errands; filters by driver, client and date. A row opens the
|
||||
* trip's quick look - where it went, the client's site and ticket, the
|
||||
* route on a map - with Return Trip to log the way back.
|
||||
*
|
||||
* Agents with Financial access see every trip; anyone else sees the trips
|
||||
* they drove or logged, and logs their own.
|
||||
*
|
||||
* Takes ?driver=, ?client= (or the client pages' ?client_id=), ?location_id=,
|
||||
* ?ticket_id=, ?show= (mine, client, ticket, own), the date filter and
|
||||
* ?open= to open a trip.
|
||||
*/
|
||||
|
||||
// Default Column Sortby/Order Filter
|
||||
$sort = "trip_date";
|
||||
$order = "DESC";
|
||||
@@ -7,209 +25,348 @@ $order = "DESC";
|
||||
// If client_id is in URI then show client Side Bar and client header
|
||||
if (isset($_GET['client_id'])) {
|
||||
require_once "includes/inc_all_client.php";
|
||||
$client_query = "AND trip_client_id = $client_id";
|
||||
$client_url = "client_id=$client_id&";
|
||||
$client_page = true;
|
||||
} else {
|
||||
require_once "includes/inc_all.php";
|
||||
$client_query = '';
|
||||
$client_url = '';
|
||||
$client_page = false;
|
||||
$client_id = 0;
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, round_trip, trip_archived_at, trip_created_at, trip_date,
|
||||
trip_destination, trip_id, trip_miles, trip_purpose, trip_source, trip_user_id, user_name, user_avatar FROM trips
|
||||
LEFT JOIN clients ON trip_client_id = client_id
|
||||
LEFT JOIN users ON trip_user_id = user_id
|
||||
WHERE (trip_purpose LIKE '%$q%' OR trip_source LIKE '%$q%' OR trip_destination LIKE '%$q%' OR trip_miles LIKE '%$q%' OR client_name LIKE '%$q%' OR user_name LIKE '%$q%')
|
||||
AND DATE(trip_date) BETWEEN '$dtf' AND '$dtt'
|
||||
AND trip_archived_at IS NULL
|
||||
$client_query
|
||||
" . clientScopeSql('trip_client_id') . "
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
$can_manage = tripCanManage();
|
||||
$sees_all = tripCanSeeAll();
|
||||
|
||||
$driver_filter = intval($_GET['driver'] ?? 0);
|
||||
$client_filter = $client_page ? intval($client_id) : intval($_GET['client'] ?? 0);
|
||||
$location_filter = intval($_GET['location_id'] ?? 0);
|
||||
$ticket_filter = intval($_GET['ticket_id'] ?? 0);
|
||||
$shows = tripShows();
|
||||
$show = (string) ($_GET['show'] ?? '');
|
||||
$show = isset($shows[$show]) ? $show : '';
|
||||
|
||||
// Only these sort - filter_header.php hands through any lowercase word
|
||||
$sortable_columns = ['trip_date' => 'trips.trip_date', 'user_name' => 'driver.user_name', 'trip_destination' => 'trips.trip_destination', 'client_name' => 'client_name', 'trip_miles' => 'trips.trip_miles'];
|
||||
if (!isset($sortable_columns[$sort])) {
|
||||
$sort = 'trip_date';
|
||||
}
|
||||
|
||||
// Everything but the pill, then the pill - the pill counts are over what the rest leave
|
||||
$filter_src = array_merge($_GET, ['dtf' => $dtf, 'dtt' => $dtt, 'show' => '', 'client' => $client_filter]);
|
||||
$where = tripFiltersSql($filter_src);
|
||||
$show_query = $shows[$show]['where'] ? "AND {$shows[$show]['where']}" : '';
|
||||
$joins = tripJoinsSql();
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS trips.*, client_name, driver.user_name AS driver_name, driver.user_avatar AS driver_avatar,
|
||||
location_name, ticket_prefix, ticket_number, ticket_subject, ticket_resolved_at, ticket_closed_at
|
||||
FROM trips $joins
|
||||
WHERE $where $show_query
|
||||
ORDER BY {$sortable_columns[$sort]} $order, trips.trip_id DESC LIMIT $record_from, $record_to");
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$trips = [];
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$trips[] = $row;
|
||||
}
|
||||
|
||||
// The pills, and the miles of what is listed
|
||||
$count_sql = [];
|
||||
foreach ($shows as $key => $s) {
|
||||
$count_sql[] = $key === '' ? 'COUNT(*)' : "COALESCE(SUM({$s['where']}), 0)";
|
||||
}
|
||||
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . ", COALESCE(SUM(CASE WHEN 1 $show_query THEN trips.trip_miles END), 0)
|
||||
FROM trips $joins WHERE $where"));
|
||||
$counts = [];
|
||||
foreach (array_keys($shows) as $i => $key) {
|
||||
$counts[$key] = intval($row[$i]);
|
||||
}
|
||||
$listed_miles = floatval($row[count($shows)]);
|
||||
|
||||
$stats = tripStats();
|
||||
$filter_url = function ($changes) use ($client_page, $client_id) {
|
||||
$params = array_merge($_GET, $changes);
|
||||
unset($params['page']);
|
||||
if ($client_page) {
|
||||
$params['client_id'] = $client_id;
|
||||
}
|
||||
return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null; }));
|
||||
};
|
||||
$date_url = function ($from, $to, $changes = []) use ($filter_url) {
|
||||
return $filter_url(array_merge(['canned_date' => 'custom', 'dtf' => $from, 'dtt' => $to], $changes));
|
||||
};
|
||||
$any_filter = $q || $driver_filter || ($client_filter && !$client_page) || $location_filter || $ticket_filter || $show !== '' || $dtf !== '1970-01-01';
|
||||
$add_url = 'modals/trip/trip_add.php' . ($client_page ? "?client_id=$client_id" : '');
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fa fa-route me-2"></i>Trips</h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/trip/trip_add.php?<?= $client_url ?>"><i class="fas fa-plus me-2"></i>New Trip</button>
|
||||
<?php if ($num_rows[0] > 0) { ?>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#"
|
||||
data-modal-url="<?= buildExportModalUrl('modals/trip/trip_export.php', ['client_id', 'q'], ['dtf' => $dtf, 'dtt' => $dtt]) ?>">
|
||||
<i class="fa fa-fw fa-download me-2"></i>Export
|
||||
</a>
|
||||
</div>
|
||||
<!-- The driving -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<a class="itflow-billing-stat <?= $dtf === $stats['month_start'] && $show === '' ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['month_start'], date('Y-m-t'), ['show' => ''])) ?>">
|
||||
<div class="itflow-billing-stat-label"><i class="fas fa-calendar-alt me-1"></i>This month</div>
|
||||
<div class="itflow-billing-stat-value"><?= escapeHtml(tripMilesText($stats['month_miles'])) ?></div>
|
||||
<div class="itflow-billing-stat-note">
|
||||
<?= intval($stats['month_trips']) ?> trip<?= intval($stats['month_trips']) === 1 ? '' : 's' ?>
|
||||
<?php if ($stats['change'] !== null) { ?>
|
||||
· <span class="<?= $stats['change'] > 0 ? 'text-warning-emphasis' : 'text-success-emphasis' ?>"><i class="fas fa-arrow-<?= $stats['change'] > 0 ? 'up' : 'down' ?> me-1"></i><?= abs($stats['change']) ?>%</span> on the same days of <?= date('F', strtotime($stats['last_start'])) ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<a class="itflow-billing-stat <?= $dtf === $stats['year_start'] && $show === '' ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['year_start'], date('Y-12-31'), ['show' => ''])) ?>">
|
||||
<div class="itflow-billing-stat-label"><i class="fas fa-chart-line me-1"></i><?= date('Y') ?> so far</div>
|
||||
<div class="itflow-billing-stat-value"><?= escapeHtml(tripMilesText($stats['year_miles'])) ?></div>
|
||||
<div class="itflow-billing-stat-note"><?= intval($stats['year_trips']) ?> trip<?= intval($stats['year_trips']) === 1 ? '' : 's' ?></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<a class="itflow-billing-stat <?= $show === 'mine' && $dtf === $stats['month_start'] ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['month_start'], date('Y-m-t'), ['show' => 'mine'])) ?>">
|
||||
<div class="itflow-billing-stat-label"><i class="fas fa-user me-1"></i>Yours this month</div>
|
||||
<div class="itflow-billing-stat-value"><?= escapeHtml(tripMilesText($stats['my_month_miles'])) ?></div>
|
||||
<div class="itflow-billing-stat-note"><?= intval($stats['my_month_trips']) ?> trip<?= intval($stats['my_month_trips']) === 1 ? '' : 's' ?> you drove</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<a class="itflow-billing-stat <?= $show === 'client' && $dtf === $stats['year_start'] ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['year_start'], date('Y-12-31'), ['show' => 'client'])) ?>">
|
||||
<div class="itflow-billing-stat-label"><i class="fas fa-building me-1"></i>For clients this year</div>
|
||||
<div class="itflow-billing-stat-value"><?= escapeHtml(tripMilesText($stats['year_client_miles'])) ?></div>
|
||||
<div class="itflow-billing-stat-note"><?= $stats['year_miles'] > 0 ? round($stats['year_client_miles'] / $stats['year_miles'] * 100) . '% of the driving' : 'No driving yet this year' ?></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-route me-2"></i>Trips<?= $sees_all ? '' : ' <span class="fw-normal text-white-50 small ms-1">yours</span>' ?></h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $add_url ?>"><i class="fas fa-plus me-2"></i>New Trip</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More"></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="<?= buildExportModalUrl('modals/trip/trip_export.php', ['client_id', 'client', 'driver', 'location_id', 'ticket_id', 'show', 'q'], ['dtf' => $dtf, 'dtt' => $dtt]) ?>">
|
||||
<i class="fa fa-fw fa-download text-secondary me-2"></i>Export
|
||||
</a>
|
||||
<?php if (lookupUserPermission('module_reporting') >= 1 && lookupUserPermission('module_financial') >= 1) { ?>
|
||||
<a class="dropdown-item" href="reports/mileage.php"><i class="fa fa-fw fa-chart-bar text-secondary me-2"></i>Mileage Report</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($client_url) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Trips">
|
||||
<button class="btn btn-secondary" title="More filters" type="button" data-bs-toggle="collapse" data-bs-target="#advancedFilter"><i class="fas fa-filter"></i></button>
|
||||
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="float-end">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-3 <?php if (isset($_GET['dtf']) && $_GET['dtf'] !== '1970-01-01') { echo"show"; } ?>" id="advancedFilter">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div>
|
||||
<label class="form-label">Date range</label>
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date']) ?? '' ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=trip_date&order=<?= $disp ?>">
|
||||
Date <?php if ($sort == 'trip_date') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=user_name&order=<?= $disp ?>">
|
||||
Driver <?php if ($sort == 'user_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=trip_purpose&order=<?= $disp ?>">
|
||||
Purpose <?php if ($sort == 'trip_purpose') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=trip_source&order=<?= $disp ?>">
|
||||
Source <?php if ($sort == 'trip_source') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=trip_destination&order=<?= $disp ?>">
|
||||
Destination <?php if ($sort == 'trip_destination') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=trip_miles&order=<?= $disp ?>">
|
||||
Miles <?php if ($sort == 'trip_miles') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php if (!$client_url) { ?>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
|
||||
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$trip_id = intval($row['trip_id']);
|
||||
$trip_date = escapeHtml($row['trip_date']);
|
||||
$trip_purpose = escapeHtml($row['trip_purpose']);
|
||||
$trip_source = escapeHtml($row['trip_source']);
|
||||
$trip_destination = escapeHtml($row['trip_destination']);
|
||||
$trip_miles = number_format(floatval($row['trip_miles']),1);
|
||||
$trip_user_id = intval($row['trip_user_id']);
|
||||
$trip_created_at = escapeHtml($row['trip_created_at']);
|
||||
$trip_archived_at = escapeHtml($row['trip_archived_at']);
|
||||
$round_trip = escapeHtml($row['round_trip']);
|
||||
$client_id = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
if (empty($client_name)) {
|
||||
$client_name_display = "-";
|
||||
} else {
|
||||
$client_name_display = "<a href='trips.php?client_id=$client_id'>$client_name</a>";
|
||||
}
|
||||
if ($round_trip == 1) {
|
||||
$round_trip_display = "<i class='fa fa-fw fa-sync-alt text-secondary'></i>";
|
||||
} else {
|
||||
$round_trip_display = "";
|
||||
}
|
||||
$user_name_display = formatAssignee($trip_user_id, $row['user_name'], $row['user_avatar'], false, 'No driver');
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<a class="text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/trip/trip_edit.php?<?= $client_url ?>&id=<?= $trip_id ?>">
|
||||
<?= $trip_date ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $user_name_display ?></td>
|
||||
<td><?= $trip_purpose ?></td>
|
||||
<td><?= $trip_source ?></td>
|
||||
<td><?= $trip_destination ?></td>
|
||||
<td><?= "<span class='text-end font-monospace'>$trip_miles</span> $round_trip_display" ?></td>
|
||||
<?php if (!$client_url) { ?>
|
||||
<td><?= $client_name_display ?></td>
|
||||
<?php } ?>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="//maps.google.com?q=<?= $trip_source ?> to <?= $trip_destination ?>" target="_blank">
|
||||
<i class="fa fa-fw fa-map-marker-alt me-2"></i>Map it<i class="fa fa-fw fa-external-link-alt ms-2"></i>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/trip/trip_edit.php?<?= $client_url ?>&id=<?= $trip_id ?>">
|
||||
<i class="fa fa-fw fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#"
|
||||
data-modal-url="modals/trip/trip_copy.php?<?= $client_url ?>&id=<?= $trip_id ?>">
|
||||
<i class="fa fa-fw fa-copy me-2"></i>Copy
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_trip=<?= $trip_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fa fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<input type="hidden" name="show" value="<?= escapeHtml($show) ?>">
|
||||
<?php if ($client_page) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
<?php foreach (['location_id' => $location_filter, 'ticket_id' => $ticket_filter] as $key => $value) {
|
||||
if ($value) { ?>
|
||||
<input type="hidden" name="<?= $key ?>" value="<?= $value ?>">
|
||||
<?php }
|
||||
} ?>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search trips" title="Purpose, from, to, client, site, driver, ticket">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<input type="text" id="dateFilter" class="form-control" autocomplete="off" title="Date of the trip" aria-label="Date of the trip">
|
||||
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date'] ?? '') ?>">
|
||||
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
|
||||
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-lg-<?= $client_page ? 6 : 3 ?> col-md-6">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<select class="form-select select2" name="driver" onchange="this.form.submit()" aria-label="Driver">
|
||||
<option value="">- Any driver -</option>
|
||||
<?php
|
||||
$sql_filter = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE EXISTS (SELECT 1 FROM trips WHERE trip_user_id = user_id) ORDER BY user_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_filter)) {
|
||||
$option_id = intval($row['user_id']); ?>
|
||||
<option <?= $driver_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionUser($row['user_id'], $row['user_name'], $row['user_avatar'] ?? '') ?>><?= escapeHtml($row['user_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if ($client_page && $can_manage) { ?>
|
||||
<?php require __DIR__ . '/includes/trip_bulk_menu.php'; ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$client_page) { ?>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<select class="form-select select2" name="client" onchange="this.form.submit()" aria-label="Client">
|
||||
<option value="">- Any client -</option>
|
||||
<?php
|
||||
$sql_filter = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients
|
||||
WHERE EXISTS (SELECT 1 FROM trips WHERE trip_client_id = client_id) " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_filter)) {
|
||||
$option_id = intval($row['client_id']); ?>
|
||||
<option <?= $client_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if ($can_manage) { ?>
|
||||
<?php require __DIR__ . '/includes/trip_bulk_menu.php'; ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Cut the list - counted over what the filters above leave -->
|
||||
<nav class="nav nav-pills itflow-asset-tabs mt-2 flex-nowrap overflow-auto">
|
||||
<?php foreach ($shows as $key => $s) { ?>
|
||||
<a class="nav-link text-nowrap <?= $show === $key ? 'active' : '' ?>" href="<?= escapeHtml($filter_url(['show' => $key])) ?>"<?= !empty($s['title']) ? ' title="' . escapeHtml($s['title']) . '"' : '' ?>>
|
||||
<?php if (!empty($s['icon'])) { ?><i class="fas fa-fw <?= $s['icon'] ?> me-1"></i><?php } ?><?= $s['label'] ?>
|
||||
<span class="badge rounded-pill ms-1"><?= $counts[$key] ?></span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</nav>
|
||||
<?php if ($location_filter || $ticket_filter) { ?>
|
||||
<div class="small text-body-secondary mt-2">
|
||||
<i class="fas fa-filter me-1"></i><?= $location_filter ? 'To the site ' . escapeHtml((string) getFieldById('locations', $location_filter, 'location_name')) : 'For the ticket ' . escapeHtml((string) getFieldById('tickets', $ticket_filter, 'ticket_subject')) ?>
|
||||
<a class="ms-2" href="<?= escapeHtml($filter_url(['location_id' => '', 'ticket_id' => ''])) ?>">Clear</a>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 itflow-billing-table">
|
||||
<thead class="<?= $num_rows[0] ? '' : 'd-none' ?> text-nowrap table-light">
|
||||
<tr>
|
||||
<?php if ($can_manage) { ?>
|
||||
<th class="checkbox-column border-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
|
||||
</div>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=trip_date&order=<?= $disp ?>">Date <?php if ($sort == 'trip_date') { echo $order_icon; } ?></a></th>
|
||||
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=trip_destination&order=<?= $disp ?>">Trip <?php if ($sort == 'trip_destination') { echo $order_icon; } ?></a></th>
|
||||
<?php if (!$client_page) { ?>
|
||||
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">Client <?php if ($sort == 'client_name') { echo $order_icon; } ?></a></th>
|
||||
<?php } else { ?>
|
||||
<th>Site</th>
|
||||
<?php } ?>
|
||||
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=user_name&order=<?= $disp ?>">Driver <?php if ($sort == 'user_name') { echo $order_icon; } ?></a></th>
|
||||
<th class="text-end"><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=trip_miles&order=<?= $disp ?>"><?= ucfirst(tripUnit()) ?> <?php if ($sort == 'trip_miles') { echo $order_icon; } ?></a></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($trips as $row) {
|
||||
$trip_id = intval($row['trip_id']);
|
||||
$row_open = 'data-modal-size="lg" data-modal-url="modals/trip/trip.php?id=' . $trip_id . '"';
|
||||
?>
|
||||
<tr>
|
||||
<?php if ($can_manage) { ?>
|
||||
<td class="checkbox-column bg-light border-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="trip_ids[]" value="<?= $trip_id ?>" aria-label="Select this trip">
|
||||
</div>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<td class="text-nowrap"><a class="fw-semibold ajax-modal" href="#" <?= $row_open ?>><?= escapeHtml($row['trip_date']) ?></a></td>
|
||||
<td class="itflow-trip-route">
|
||||
<div class="text-truncate" title="<?= escapeHtml($row['trip_source'] . ' → ' . $row['trip_destination']) ?>">
|
||||
<span class="text-body-secondary"><?= escapeHtml($row['trip_source']) ?></span>
|
||||
<i class="fas fa-long-arrow-alt-right text-body-tertiary mx-1"></i><?= escapeHtml($row['trip_destination']) ?>
|
||||
</div>
|
||||
<div class="small text-body-secondary text-truncate" title="<?= escapeHtml($row['trip_purpose']) ?>">
|
||||
<?php if (intval($row['trip_ticket_id']) && $row['ticket_number'] !== null) { ?>
|
||||
<a class="font-monospace" href="<?= escapeHtml(ticketUrl($row['trip_ticket_id'], $row['trip_client_id'])) ?>" title="<?= escapeHtml($row['ticket_subject']) ?>"><?= escapeHtml(ticketReference($row)) ?></a> ·
|
||||
<?php } ?>
|
||||
<?= escapeHtml($row['trip_purpose']) ?>
|
||||
</div>
|
||||
</td>
|
||||
<?php if (!$client_page) { ?>
|
||||
<td>
|
||||
<?php if (intval($row['trip_client_id'])) { ?>
|
||||
<div class="text-truncate" style="max-width: 12rem;"><a class="text-reset" href="<?= escapeHtml($filter_url(['client' => intval($row['trip_client_id'])])) ?>" title="<?= escapeHtml((string) $row['client_name']) ?> - their trips"><?= escapeHtml($row['client_name']) ?></a></div>
|
||||
<?php if ($row['location_name'] !== null) { ?><div class="small text-body-secondary text-truncate" style="max-width: 12rem;"><i class="fas fa-map-marker-alt me-1"></i><?= escapeHtml($row['location_name']) ?></div><?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary small">Our own</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php } else { ?>
|
||||
<td class="small"><?= $row['location_name'] !== null ? '<a class="text-reset" href="' . escapeHtml($filter_url(['location_id' => intval($row['trip_location_id'])])) . '"><i class="fas fa-map-marker-alt text-body-secondary me-1"></i>' . escapeHtml($row['location_name']) . '</a>' : '<span class="text-body-secondary">-</span>' ?></td>
|
||||
<?php } ?>
|
||||
<td class="text-nowrap"><?= formatAssignee($row['trip_user_id'], $row['driver_name'] ?? '', $row['driver_avatar'] ?? '', false, 'No driver') ?></td>
|
||||
<td class="text-end text-nowrap">
|
||||
<span class="fw-semibold"><?= number_format(floatval($row['trip_miles']), 1) ?></span>
|
||||
<?php if (intval($row['round_trip'])) { ?><i class="fas fa-sync-alt text-body-tertiary small ms-1" title="Round trip - <?= escapeHtml(tripMilesText(tripOneWay($row))) ?> each way"></i><?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" <?= $row_open ?>><i class="fas fa-fw fa-eye text-secondary me-2"></i>Open</a>
|
||||
<a class="dropdown-item" href="<?= escapeHtml(tripMapUrl($row)) ?>" target="_blank" rel="noopener"><i class="fas fa-fw fa-map-marked-alt text-secondary me-2"></i>Directions<i class="fas fa-external-link-alt small text-secondary ms-2"></i></a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if (tripCanEdit($row)) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip_edit.php?id=<?= $trip_id ?>"><i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip_copy.php?id=<?= $trip_id ?>"><i class="fas fa-fw fa-copy text-secondary me-2"></i>Copy</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip_copy.php?id=<?= $trip_id ?>&return=1"><i class="fas fa-fw fa-undo-alt text-secondary me-2"></i>Return Trip</a>
|
||||
<?php if (tripCanDelete($row)) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_trip=<?= $trip_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||||
data-confirm-text="<?= escapeHtml(tripLabel($row)) ?> goes for good.">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
<?php if ($num_rows[0]) { ?>
|
||||
<tfoot>
|
||||
<tr class="table-light">
|
||||
<td colspan="<?= $can_manage ? 5 : 4 ?>" class="text-body-secondary small">
|
||||
<?= intval($num_rows[0]) ?> trip<?= intval($num_rows[0]) === 1 ? '' : 's' ?><?= $any_filter ? ' match' . (intval($num_rows[0]) === 1 ? 'es' : '') : '' ?><?= $num_rows[0] > count($trips) ? ' - the total is for all of them, not just this page' : '' ?>
|
||||
</td>
|
||||
<td class="text-end fw-semibold text-nowrap"><?= escapeHtml(tripMilesText($listed_miles)) ?></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<?php } ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (!$num_rows[0]) { ?>
|
||||
<div class="itflow-asset-empty">
|
||||
<i class="fas fa-route fa-2x mb-2 opacity-50"></i>
|
||||
<div><?= $any_filter ? 'No trips match.' : 'No trips yet.' ?></div>
|
||||
<?php if (!$any_filter) { ?>
|
||||
<div class="small mt-1">Log the drive out to a client from their ticket - Log a trip, in its menu - or with New Trip here.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($_GET['open'])) { ?>
|
||||
<script>
|
||||
// Opened from elsewhere - straight to that trip's quick look
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var open = document.createElement('a');
|
||||
open.className = 'ajax-modal';
|
||||
open.href = '#';
|
||||
open.dataset.modalSize = 'lg';
|
||||
open.dataset.modalUrl = 'modals/trip/trip.php?id=<?= intval($_GET['open']) ?>';
|
||||
document.body.appendChild(open);
|
||||
open.click();
|
||||
open.remove();
|
||||
});
|
||||
</script>
|
||||
<?php } ?>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
|
||||
@@ -6362,3 +6362,9 @@ a.itflow-billing-stat:hover {
|
||||
z-index: 3;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
}
|
||||
/* Trips - the route and what for take the room left and cut short, rather than push the miles off the side (agent/trips.php, a site's Trips tab) */
|
||||
.itflow-trip-route {
|
||||
width: 100%;
|
||||
max-width: 0;
|
||||
min-width: 14rem;
|
||||
}
|
||||
|
||||
12
db.sql
12
db.sql
@@ -3287,16 +3287,22 @@ CREATE TABLE `trips` (
|
||||
`trip_source` varchar(200) NOT NULL,
|
||||
`trip_destination` varchar(200) NOT NULL,
|
||||
`trip_start_odometer` int(11) DEFAULT NULL,
|
||||
`trip_end_odmeter` int(11) DEFAULT NULL,
|
||||
`trip_miles` float(15,1) NOT NULL,
|
||||
`trip_end_odometer` int(11) DEFAULT NULL,
|
||||
`trip_miles` decimal(10,1) NOT NULL DEFAULT 0.0,
|
||||
`round_trip` int(1) NOT NULL,
|
||||
`trip_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`trip_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||
`trip_archived_at` datetime DEFAULT NULL,
|
||||
`trip_user_id` int(11) NOT NULL DEFAULT 0,
|
||||
`trip_client_id` int(11) NOT NULL DEFAULT 0,
|
||||
`trip_location_id` int(11) NOT NULL DEFAULT 0,
|
||||
`trip_ticket_id` int(11) NOT NULL DEFAULT 0,
|
||||
`trip_created_by` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`trip_id`),
|
||||
KEY `trip_client_id` (`trip_client_id`,`trip_archived_at`)
|
||||
KEY `trip_client_id` (`trip_client_id`,`trip_archived_at`),
|
||||
KEY `trip_user_id` (`trip_user_id`,`trip_date`),
|
||||
KEY `trip_ticket_id` (`trip_ticket_id`),
|
||||
KEY `trip_location_id` (`trip_location_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
@@ -55,3 +55,4 @@ require_once __DIR__ . '/functions/products.php';
|
||||
require_once __DIR__ . '/functions/billing_email.php';
|
||||
require_once __DIR__ . '/functions/expenses.php';
|
||||
require_once __DIR__ . '/functions/expense_import.php';
|
||||
require_once __DIR__ . '/functions/trips.php';
|
||||
|
||||
@@ -420,11 +420,18 @@ function getExportColumns($export_type) {
|
||||
|
||||
'trips' => [
|
||||
'trip_date' => ['label' => 'Date'],
|
||||
'user_name' => ['label' => 'Driver', 'weight' => 2],
|
||||
'trip_purpose' => ['label' => 'Purpose', 'weight' => 3],
|
||||
'trip_source' => ['label' => 'Source', 'weight' => 2],
|
||||
'trip_destination' => ['label' => 'Destination', 'weight' => 2],
|
||||
'trip_source' => ['label' => 'From', 'weight' => 2],
|
||||
'trip_destination' => ['label' => 'To', 'weight' => 2],
|
||||
'trip_miles' => ['label' => 'Miles', 'format' => 'number'],
|
||||
'round_trip_display' => ['label' => 'Round Trip', 'default' => false],
|
||||
'trip_one_way' => ['label' => 'One Way', 'format' => 'number', 'default' => false],
|
||||
'client_name' => ['label' => 'Client', 'weight' => 2, 'default' => false],
|
||||
'location_name' => ['label' => 'Site', 'weight' => 2, 'default' => false],
|
||||
'ticket_display' => ['label' => 'Ticket', 'weight' => 2, 'default' => false],
|
||||
'trip_start_odometer' => ['label' => 'Odometer Start', 'default' => false],
|
||||
'trip_end_odometer' => ['label' => 'Odometer End', 'default' => false],
|
||||
],
|
||||
|
||||
// user_status_display is the status word, built by the handler
|
||||
|
||||
@@ -216,6 +216,7 @@ function locationClearReferences($location_id) {
|
||||
}
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_location_id = 0 WHERE ticket_location_id = $location_id");
|
||||
mysqli_query($mysqli, "UPDATE calendar_events SET event_location_id = 0 WHERE event_location_id = $location_id");
|
||||
mysqli_query($mysqli, "UPDATE trips SET trip_location_id = 0 WHERE trip_location_id = $location_id");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -792,7 +792,7 @@ function globalSearchPages($raw_query) {
|
||||
['Income', '/agent/income.php', $financial && $accounting],
|
||||
['Expenses', '/agent/expenses.php', $financial && $accounting],
|
||||
['Recurring Expenses', '/agent/recurring_expenses.php', $financial && $accounting],
|
||||
['Trips', '/agent/trips.php', $financial && $accounting],
|
||||
['Trips', '/agent/trips.php', $accounting],
|
||||
['Accounts', '/agent/accounts.php', $financial && $accounting],
|
||||
['Transactions', '/agent/transactions.php', $financial && $accounting],
|
||||
['Transfers', '/agent/transfers.php', $financial && $accounting],
|
||||
|
||||
@@ -283,6 +283,8 @@ function ticketDeleteForGood($ticket_id) {
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_history WHERE ticket_history_ticket_id = $ticket_id");
|
||||
mysqli_query($mysqli, "DELETE FROM sla_history WHERE sla_history_ticket_id = $ticket_id");
|
||||
mysqli_query($mysqli, "DELETE FROM tickets WHERE ticket_id = $ticket_id");
|
||||
// The driving for it still happened - the trips stay, without the ticket
|
||||
mysqli_query($mysqli, "UPDATE trips SET trip_ticket_id = 0 WHERE trip_ticket_id = $ticket_id");
|
||||
removeDirectory(__DIR__ . "/../uploads/tickets/$ticket_id");
|
||||
}
|
||||
|
||||
|
||||
491
functions/trips.php
Normal file
491
functions/trips.php
Normal file
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - trips: the driving an MSP does, for clients and for itself.
|
||||
*
|
||||
* A trip is a date, where from and where to, the miles (stored as the whole
|
||||
* trip - one way doubled for a round trip), what it was for and who drove.
|
||||
* For a client it can name the client's site it went to (trip_location_id)
|
||||
* and the ticket it was for (trip_ticket_id), so a ticket and a site show
|
||||
* their trips. Odometer readings are optional; when both are given they
|
||||
* make the miles.
|
||||
*
|
||||
* Who sees what: agents with Financial access see every trip in their
|
||||
* client scope; anyone else sees the trips they drove or logged. Anyone can
|
||||
* log a trip for themselves and change or delete their own; logging one for
|
||||
* someone else, or changing theirs, needs Financial write access, and
|
||||
* deleting theirs full access.
|
||||
*/
|
||||
|
||||
/** The distance a trip is measured in - "miles", or "mi" short. */
|
||||
function tripUnit($short = false) {
|
||||
return $short ? 'mi' : 'miles';
|
||||
}
|
||||
|
||||
/** "12.4 mi" */
|
||||
function tripMilesText($miles) {
|
||||
return number_format(floatval($miles), 1) . ' ' . tripUnit(true);
|
||||
}
|
||||
|
||||
/** May this agent see every trip (in their client scope), not just their own? */
|
||||
function tripCanSeeAll() {
|
||||
return lookupUserPermission('module_financial') >= 1;
|
||||
}
|
||||
|
||||
/** May this agent log trips for others and change theirs? */
|
||||
function tripCanManage() {
|
||||
return lookupUserPermission('module_financial') >= 2;
|
||||
}
|
||||
|
||||
/** Is it this agent's own trip - they drove it or logged it? */
|
||||
function tripIsMine(array $row) {
|
||||
global $session_user_id;
|
||||
return intval($row['trip_user_id']) === intval($session_user_id) || intval($row['trip_created_by'] ?? 0) === intval($session_user_id);
|
||||
}
|
||||
|
||||
/** May this agent change this trip? */
|
||||
function tripCanEdit(array $row) {
|
||||
return tripIsMine($row) || tripCanManage();
|
||||
}
|
||||
|
||||
/** May this agent delete this trip? */
|
||||
function tripCanDelete(array $row) {
|
||||
return tripIsMine($row) || lookupUserPermission('module_financial') >= 3;
|
||||
}
|
||||
|
||||
/** The trips this agent may see, as SQL - their client scope, and their own unless they see all. */
|
||||
function tripScopeSql() {
|
||||
global $session_user_id;
|
||||
$sql = clientScopeSql('trips.trip_client_id');
|
||||
if (!tripCanSeeAll()) {
|
||||
$me = intval($session_user_id);
|
||||
$sql .= " AND (trips.trip_user_id = $me OR trips.trip_created_by = $me)";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/** The joins the list, export and quick look read through. */
|
||||
function tripJoinsSql() {
|
||||
return "LEFT JOIN clients ON client_id = trips.trip_client_id
|
||||
LEFT JOIN users AS driver ON driver.user_id = trips.trip_user_id
|
||||
LEFT JOIN locations ON location_id = trips.trip_location_id
|
||||
LEFT JOIN tickets ON ticket_id = trips.trip_ticket_id";
|
||||
}
|
||||
|
||||
/** One trip with its client, driver, site, ticket and who logged it - raw, or null. Scoped to what this agent may see. */
|
||||
function tripRow($trip_id, $scoped = true) {
|
||||
global $mysqli;
|
||||
$trip_id = intval($trip_id);
|
||||
if (!$trip_id) {
|
||||
return null;
|
||||
}
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT trips.*, client_name, client_abbreviation, driver.user_name AS driver_name, driver.user_avatar AS driver_avatar,
|
||||
creator.user_name AS created_by_name, location_name, location_address, location_city, location_state, location_zip, location_archived_at,
|
||||
ticket_prefix, ticket_number, ticket_subject, ticket_client_id, ticket_resolved_at, ticket_closed_at, ticket_onsite
|
||||
FROM trips " . tripJoinsSql() . "
|
||||
LEFT JOIN users AS creator ON creator.user_id = trips.trip_created_by
|
||||
WHERE trips.trip_id = $trip_id AND trips.trip_archived_at IS NULL " . ($scoped ? tripScopeSql() : '') . "
|
||||
LIMIT 1"));
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/** The miles one way - the whole trip halved for a round trip. */
|
||||
function tripOneWay(array $row) {
|
||||
$miles = floatval($row['trip_miles']);
|
||||
return intval($row['round_trip']) ? round($miles / 2, 1) : $miles;
|
||||
}
|
||||
|
||||
/** A trip in a sentence - "Office to Ravenwood Dental on 2026-10-14 (24.6 mi)". Raw. */
|
||||
function tripLabel(array $row) {
|
||||
return $row['trip_source'] . ' to ' . $row['trip_destination'] . ' on ' . $row['trip_date'] . ' (' . tripMilesText($row['trip_miles']) . ')';
|
||||
}
|
||||
|
||||
/** A directions link for the trip - to the address, from where it started. */
|
||||
function tripMapUrl(array $row) {
|
||||
return 'https://www.google.com/maps/dir/?api=1&origin=' . rawurlencode((string) $row['trip_source']) . '&destination=' . rawurlencode((string) $row['trip_destination']);
|
||||
}
|
||||
|
||||
/** Where we start from - the company's address, as one line, or ''. */
|
||||
function tripOfficeAddress() {
|
||||
global $mysqli;
|
||||
static $address = null;
|
||||
if ($address === null) {
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT company_address, company_city, company_state, company_zip FROM companies WHERE company_id = 1")) ?: [];
|
||||
$address = formatAddress($row['company_address'] ?? '', $row['company_city'] ?? '', $row['company_state'] ?? '', $row['company_zip'] ?? '', '', ' ');
|
||||
}
|
||||
return $address;
|
||||
}
|
||||
|
||||
/** A client site's address as one line - its name when it has no address. */
|
||||
function tripLocationAddress(array $row) {
|
||||
$address = formatAddress($row['location_address'] ?? '', $row['location_city'] ?? '', $row['location_state'] ?? '', $row['location_zip'] ?? '', '', ' ');
|
||||
return $address !== '' ? $address : (string) ($row['location_name'] ?? '');
|
||||
}
|
||||
|
||||
/** The ways the list can be cut - ?show= */
|
||||
function tripShows() {
|
||||
global $session_user_id;
|
||||
$me = intval($session_user_id);
|
||||
return [
|
||||
'' => ['label' => 'All', 'where' => ''],
|
||||
'mine' => ['label' => 'Mine', 'icon' => 'fa-user', 'where' => "trips.trip_user_id = $me", 'title' => 'Trips you drove'],
|
||||
'client' => ['label' => 'For a client', 'icon' => 'fa-building', 'where' => 'trips.trip_client_id > 0', 'title' => 'Driving for a client'],
|
||||
'ticket' => ['label' => 'For a ticket', 'icon' => 'fa-life-ring', 'where' => 'trips.trip_ticket_id > 0', 'title' => 'Onsite work on a ticket'],
|
||||
'own' => ['label' => 'Our own', 'icon' => 'fa-store', 'where' => 'trips.trip_client_id = 0', 'title' => "Errands for the business - the bank, the post office, a supplier"],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The WHERE for a set of filters, as the list and the export take them: q,
|
||||
* driver, client, location_id, ticket_id, show, dtf and dtt. Starts with what
|
||||
* this agent may see. $summary gets what each is, by name, for the export.
|
||||
*/
|
||||
function tripFiltersSql(array $src, &$summary = []) {
|
||||
$summary = [];
|
||||
$sql = "trips.trip_archived_at IS NULL " . tripScopeSql();
|
||||
$q = trim((string) ($src['q'] ?? ''));
|
||||
if ($q !== '') {
|
||||
$like = escapeSql($q);
|
||||
$sql .= " AND (trips.trip_purpose LIKE '%$like%' OR trips.trip_source LIKE '%$like%' OR trips.trip_destination LIKE '%$like%' OR client_name LIKE '%$like%'
|
||||
OR driver.user_name LIKE '%$like%' OR location_name LIKE '%$like%' OR CONCAT(ticket_prefix, ticket_number) LIKE '%$like%')";
|
||||
$summary['Search'] = $q;
|
||||
}
|
||||
foreach (['driver' => ['trips.trip_user_id', 'users', 'user_name', 'Driver'], 'client' => ['trips.trip_client_id', 'clients', 'client_name', 'Client'],
|
||||
'location_id' => ['trips.trip_location_id', 'locations', 'location_name', 'Site'], 'ticket_id' => ['trips.trip_ticket_id', 'tickets', 'ticket_subject', 'Ticket']] as $key => $f) {
|
||||
$id = intval($src[$key] ?? 0);
|
||||
if ($id) {
|
||||
$sql .= " AND {$f[0]} = $id";
|
||||
$summary[$f[3]] = (string) getFieldById($f[1], $id, $f[2]);
|
||||
}
|
||||
}
|
||||
$shows = tripShows();
|
||||
$show = (string) ($src['show'] ?? '');
|
||||
if (isset($shows[$show]) && $shows[$show]['where']) {
|
||||
$sql .= " AND {$shows[$show]['where']}";
|
||||
$summary['Showing'] = $shows[$show]['label'];
|
||||
}
|
||||
$dtf = (string) ($src['dtf'] ?? '');
|
||||
$dtt = (string) ($src['dtt'] ?? '');
|
||||
$dtf = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dtf) ? $dtf : '1970-01-01';
|
||||
$dtt = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dtt) ? $dtt : '2099-12-31';
|
||||
if ($dtf !== '1970-01-01' || $dtt !== '2099-12-31') {
|
||||
$sql .= " AND trips.trip_date BETWEEN '$dtf' AND '$dtt'";
|
||||
$range = formatExportDateRange($dtf, $dtt);
|
||||
if ($range) {
|
||||
$summary['Dated'] = $range;
|
||||
}
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* The driving along the top of the list, in what this agent may see: miles
|
||||
* this month against the same days of last month, the year so far, and the
|
||||
* agent's own this month.
|
||||
*/
|
||||
function tripStats() {
|
||||
global $mysqli, $session_user_id;
|
||||
$scope = "trips.trip_archived_at IS NULL " . tripScopeSql();
|
||||
$month_start = date('Y-m-01');
|
||||
$today = date('Y-m-d');
|
||||
$last_start = date('Y-m-01', strtotime('first day of last month'));
|
||||
$last_to_date = date('Y-m-d', min(strtotime(date('Y-m-t', strtotime($last_start))), strtotime($last_start . ' +' . (intval(date('j')) - 1) . ' days')));
|
||||
$year_start = date('Y-01-01');
|
||||
$me = intval($session_user_id);
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT
|
||||
COALESCE(SUM(CASE WHEN trip_date BETWEEN '$month_start' AND '$today' THEN trip_miles END), 0) AS month_miles,
|
||||
COALESCE(SUM(trip_date BETWEEN '$month_start' AND '$today'), 0) AS month_trips,
|
||||
COALESCE(SUM(CASE WHEN trip_date BETWEEN '$last_start' AND '$last_to_date' THEN trip_miles END), 0) AS last_to_date,
|
||||
COALESCE(SUM(CASE WHEN trip_date BETWEEN '$year_start' AND '$today' THEN trip_miles END), 0) AS year_miles,
|
||||
COALESCE(SUM(trip_date BETWEEN '$year_start' AND '$today'), 0) AS year_trips,
|
||||
COALESCE(SUM(CASE WHEN trip_date BETWEEN '$year_start' AND '$today' AND trip_client_id > 0 THEN trip_miles END), 0) AS year_client_miles,
|
||||
COALESCE(SUM(CASE WHEN trip_date BETWEEN '$month_start' AND '$today' AND trip_user_id = $me THEN trip_miles END), 0) AS my_month_miles,
|
||||
COALESCE(SUM(trip_date BETWEEN '$month_start' AND '$today' AND trip_user_id = $me), 0) AS my_month_trips
|
||||
FROM trips LEFT JOIN clients ON client_id = trip_client_id WHERE $scope"));
|
||||
$row = array_map('floatval', $row ?: []);
|
||||
$row['month_start'] = $month_start;
|
||||
$row['last_start'] = $last_start;
|
||||
$row['year_start'] = $year_start;
|
||||
$row['change'] = $row['last_to_date'] > 0 ? round(($row['month_miles'] - $row['last_to_date']) / $row['last_to_date'] * 100) : null;
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** Who can drive - active agents, and $keep_id - id => [name, avatar]. */
|
||||
function tripDrivers($keep_id = 0) {
|
||||
global $mysqli;
|
||||
$keep_id = intval($keep_id);
|
||||
$drivers = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE (user_type = 1 AND user_archived_at IS NULL) OR user_id = $keep_id ORDER BY user_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$drivers[intval($row['user_id'])] = $row;
|
||||
}
|
||||
return $drivers;
|
||||
}
|
||||
|
||||
/** The sites of the clients this agent may reach - client_id => [[id, name, address], ...], archived ones left out but $keep_id. */
|
||||
function tripClientSites($client_id = 0, $keep_id = 0) {
|
||||
global $mysqli;
|
||||
$client_id = intval($client_id);
|
||||
$keep_id = intval($keep_id);
|
||||
$sites = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT location_id, location_client_id, location_name, location_address, location_city, location_state, location_zip, location_primary
|
||||
FROM locations JOIN clients ON client_id = location_client_id
|
||||
WHERE (location_archived_at IS NULL OR location_id = $keep_id) AND client_archived_at IS NULL " . ($client_id ? "AND location_client_id = $client_id " : '') . clientScopeSql('location_client_id') . "
|
||||
ORDER BY location_primary DESC, location_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$sites[intval($row['location_client_id'])][] = [intval($row['location_id']), $row['location_name'], tripLocationAddress($row)];
|
||||
}
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tickets a trip can be for - each client's open ones and those done in
|
||||
* the last 60 days, newest first, up to 25 a client, plus $keep_id - client_id
|
||||
* => [[id, "TCK-12 - Subject", location_id, open], ...]. Only for agents who
|
||||
* can see tickets.
|
||||
*/
|
||||
function tripClientTickets($client_id = 0, $keep_id = 0) {
|
||||
global $mysqli;
|
||||
if (lookupUserPermission('module_support') < 1) {
|
||||
return [];
|
||||
}
|
||||
$client_id = intval($client_id);
|
||||
$keep_id = intval($keep_id);
|
||||
$tickets = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_client_id, ticket_location_id, ticket_resolved_at, ticket_closed_at
|
||||
FROM tickets
|
||||
WHERE ticket_client_id > 0 AND ticket_archived_at IS NULL " . ($client_id ? "AND ticket_client_id = $client_id " : '') . "
|
||||
AND (" . TICKET_OPEN_SQL . " OR COALESCE(ticket_resolved_at, ticket_closed_at) >= CURDATE() - INTERVAL 60 DAY OR ticket_id = $keep_id) " . clientScopeSql('ticket_client_id') . "
|
||||
ORDER BY ticket_client_id, (ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) DESC, ticket_id DESC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$cid = intval($row['ticket_client_id']);
|
||||
if (count($tickets[$cid] ?? []) >= 25 && intval($row['ticket_id']) !== $keep_id) {
|
||||
continue;
|
||||
}
|
||||
$tickets[$cid][] = [intval($row['ticket_id']), ticketReference($row) . ' - ' . mb_substr((string) $row['ticket_subject'], 0, 80), intval($row['ticket_location_id']), ticketIsOpen($row) ? 1 : 0];
|
||||
}
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes driven before - "from|to" (lowercase) => miles one way, the last
|
||||
* time each was driven, from the trips this agent may see. Both directions,
|
||||
* so the way back is known too. For the form to fill in.
|
||||
*/
|
||||
function tripKnownRoutes($limit = 400) {
|
||||
global $mysqli;
|
||||
$routes = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT trip_source, trip_destination, trip_miles, round_trip FROM trips LEFT JOIN clients ON client_id = trip_client_id
|
||||
WHERE trips.trip_archived_at IS NULL AND trip_miles > 0 " . tripScopeSql() . "
|
||||
ORDER BY trip_date DESC, trip_id DESC LIMIT " . intval($limit * 3));
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$from = mb_strtolower(trim((string) $row['trip_source']));
|
||||
$to = mb_strtolower(trim((string) $row['trip_destination']));
|
||||
if ($from === '' || $to === '') {
|
||||
continue;
|
||||
}
|
||||
$one_way = tripOneWay($row);
|
||||
$routes += ["$from|$to" => $one_way, "$to|$from" => $one_way];
|
||||
if (count($routes) >= $limit * 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/** Where this agent has started from lately, most used first - for the From box to offer, with the office first. */
|
||||
function tripStartPlaces($limit = 6) {
|
||||
global $mysqli, $session_user_id;
|
||||
$places = [];
|
||||
$office = tripOfficeAddress();
|
||||
if ($office !== '') {
|
||||
$places[] = $office;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT trip_source, COUNT(*) AS n FROM trips WHERE trip_user_id = " . intval($session_user_id) . " AND trip_archived_at IS NULL
|
||||
GROUP BY trip_source ORDER BY n DESC LIMIT " . intval($limit));
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
if (!in_array($row[0], $places, true) && trim((string) $row[0]) !== '') {
|
||||
$places[] = $row[0];
|
||||
}
|
||||
}
|
||||
return $places;
|
||||
}
|
||||
|
||||
/** Where this agent's last trip started - for a new one to start from. */
|
||||
function tripLastStart() {
|
||||
global $mysqli, $session_user_id;
|
||||
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT trip_source FROM trips WHERE trip_user_id = " . intval($session_user_id) . " AND trip_archived_at IS NULL ORDER BY trip_date DESC, trip_id DESC LIMIT 1"));
|
||||
return $row ? (string) $row[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The one path that writes a trip from a form - New, Edit, Copy and Return.
|
||||
* $in: date, source, destination, miles (one way), round_trip, odometer_start,
|
||||
* odometer_end, purpose, driver, client_id, location_id, ticket_id. A ticket
|
||||
* brings its client when none is picked; a site and ticket must be the
|
||||
* client's. Returns the trip id, or 0 with $error.
|
||||
*/
|
||||
function tripSave(array $in, $trip_id = 0, &$error = '') {
|
||||
global $mysqli, $session_user_id;
|
||||
$error = '';
|
||||
$trip_id = intval($trip_id);
|
||||
$existing = $trip_id ? tripRow($trip_id) : null;
|
||||
if ($trip_id && !$existing) {
|
||||
$error = 'That trip no longer exists, or is not one you can see';
|
||||
return 0;
|
||||
}
|
||||
if ($existing && !tripCanEdit($existing)) {
|
||||
$error = "That is someone else's trip - changing it needs write access to Financial";
|
||||
return 0;
|
||||
}
|
||||
|
||||
$date = trim((string) ($in['date'] ?? ''));
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) || !checkdate(intval(substr($date, 5, 2)), intval(substr($date, 8, 2)), intval(substr($date, 0, 4)))) {
|
||||
$error = 'Enter the date of the trip';
|
||||
return 0;
|
||||
}
|
||||
$source = mb_substr(trim(preg_replace('/\s+/u', ' ', (string) ($in['source'] ?? ''))), 0, 200);
|
||||
$destination = mb_substr(trim(preg_replace('/\s+/u', ' ', (string) ($in['destination'] ?? ''))), 0, 200);
|
||||
if ($source === '' || $destination === '') {
|
||||
$error = 'Enter where the trip started and where it went';
|
||||
return 0;
|
||||
}
|
||||
$purpose = mb_substr(trim((string) ($in['purpose'] ?? '')), 0, 200);
|
||||
if ($purpose === '') {
|
||||
$error = 'Enter what the trip was for';
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The miles: from the odometer when both readings are there, else one way, doubled for a round trip
|
||||
$round = !empty($in['round_trip']) ? 1 : 0;
|
||||
$odo_start_text = trim(str_replace(',', '', (string) ($in['odometer_start'] ?? '')));
|
||||
$odo_end_text = trim(str_replace(',', '', (string) ($in['odometer_end'] ?? '')));
|
||||
if (($odo_start_text !== '' && !ctype_digit($odo_start_text)) || ($odo_end_text !== '' && !ctype_digit($odo_end_text))) {
|
||||
$error = 'The odometer readings are whole numbers';
|
||||
return 0;
|
||||
}
|
||||
$odo_start = $odo_start_text === '' ? null : intval($odo_start_text);
|
||||
$odo_end = $odo_end_text === '' ? null : intval($odo_end_text);
|
||||
if ($odo_start !== null && $odo_end !== null) {
|
||||
if ($odo_end < $odo_start) {
|
||||
$error = 'The odometer at the end is lower than at the start';
|
||||
return 0;
|
||||
}
|
||||
$miles = round($odo_end - $odo_start, 1);
|
||||
} else {
|
||||
$miles_text = trim(str_replace([',', ' '], '', (string) ($in['miles'] ?? '')));
|
||||
if (!is_numeric($miles_text) || floatval($miles_text) < 0 || floatval($miles_text) >= 100000) {
|
||||
$error = 'Enter the ' . tripUnit() . ' one way';
|
||||
return 0;
|
||||
}
|
||||
$miles = round(round(floatval($miles_text), 1) * ($round ? 2 : 1), 1);
|
||||
}
|
||||
|
||||
// The client, the ticket it was for and the site it went to
|
||||
$client_id = intval($in['client_id'] ?? 0);
|
||||
$ticket_id = intval($in['ticket_id'] ?? 0);
|
||||
if ($ticket_id) {
|
||||
$ticket = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_id, ticket_client_id FROM tickets WHERE ticket_id = $ticket_id AND ticket_client_id > 0 " . clientScopeSql('ticket_client_id')));
|
||||
if (!$ticket || ($client_id && intval($ticket['ticket_client_id']) !== $client_id) || (lookupUserPermission('module_support') < 1 && (!$existing || intval($existing['trip_ticket_id']) !== $ticket_id))) {
|
||||
$error = "That ticket isn't one of the client's";
|
||||
return 0;
|
||||
}
|
||||
$client_id = intval($ticket['ticket_client_id']);
|
||||
}
|
||||
if ($client_id && !mysqli_num_rows(mysqli_query($mysqli, "SELECT client_id FROM clients WHERE client_id = $client_id " . clientScopeSql('client_id')))) {
|
||||
$error = "That client isn't one you can log trips for";
|
||||
return 0;
|
||||
}
|
||||
$location_id = $client_id ? intval($in['location_id'] ?? 0) : 0;
|
||||
if ($location_id && !mysqli_num_rows(mysqli_query($mysqli, "SELECT location_id FROM locations WHERE location_id = $location_id AND location_client_id = $client_id"))) {
|
||||
$error = "That site isn't one of the client's";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Who drove - only themselves, unless they may log trips for others
|
||||
$driver = intval($in['driver'] ?? 0);
|
||||
$drivers = tripDrivers($existing ? $existing['trip_user_id'] : 0);
|
||||
if (!isset($drivers[$driver])) {
|
||||
$error = 'Pick who drove';
|
||||
return 0;
|
||||
}
|
||||
if (!tripCanManage() && $driver !== intval($session_user_id) && !($existing && $driver === intval($existing['trip_user_id']))) {
|
||||
$error = 'You can log your own trips - logging one for someone else needs write access to Financial';
|
||||
return 0;
|
||||
}
|
||||
|
||||
$set = "trip_date = '$date', trip_source = '" . escapeSql($source) . "', trip_destination = '" . escapeSql($destination) . "', trip_miles = $miles, round_trip = $round,
|
||||
trip_start_odometer = " . ($odo_start === null ? 'NULL' : $odo_start) . ", trip_end_odometer = " . ($odo_end === null ? 'NULL' : $odo_end) . ",
|
||||
trip_purpose = '" . escapeSql($purpose) . "', trip_user_id = $driver, trip_client_id = $client_id, trip_location_id = $location_id, trip_ticket_id = $ticket_id";
|
||||
if ($existing) {
|
||||
mysqli_query($mysqli, "UPDATE trips SET $set WHERE trip_id = $trip_id");
|
||||
return $trip_id;
|
||||
}
|
||||
mysqli_query($mysqli, "INSERT INTO trips SET $set, trip_created_by = " . intval($session_user_id));
|
||||
return mysqli_insert_id($mysqli);
|
||||
}
|
||||
|
||||
/** Delete a trip this agent may delete - the row it deleted, or null with $error. */
|
||||
function tripDelete($trip_id, &$error = '') {
|
||||
global $mysqli;
|
||||
$error = '';
|
||||
$row = tripRow($trip_id);
|
||||
if (!$row) {
|
||||
$error = 'That trip no longer exists, or is not one you can see';
|
||||
return null;
|
||||
}
|
||||
if (!tripCanDelete($row)) {
|
||||
$error = "That is someone else's trip - deleting it needs full access to Financial";
|
||||
return null;
|
||||
}
|
||||
mysqli_query($mysqli, "DELETE FROM trips WHERE trip_id = " . intval($row['trip_id']));
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** The trips of a ticket, a site or a client - newest first, scoped. For the cards that list them. */
|
||||
function tripsFor($column, $id, $limit = 10) {
|
||||
global $mysqli;
|
||||
if (!in_array($column, ['trip_ticket_id', 'trip_location_id', 'trip_client_id'], true)) {
|
||||
return [];
|
||||
}
|
||||
$id = intval($id);
|
||||
$rows = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT trips.*, driver.user_name AS driver_name, driver.user_avatar AS driver_avatar, client_name, location_name, ticket_prefix, ticket_number, ticket_subject
|
||||
FROM trips " . tripJoinsSql() . "
|
||||
WHERE trips.$column = $id AND trips.trip_archived_at IS NULL " . tripScopeSql() . "
|
||||
ORDER BY trip_date DESC, trip_id DESC LIMIT " . intval($limit));
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/** How many trips and miles a ticket, site or client has - [count, miles], scoped. */
|
||||
function tripTotalsFor($column, $id) {
|
||||
global $mysqli;
|
||||
if (!in_array($column, ['trip_ticket_id', 'trip_location_id', 'trip_client_id'], true)) {
|
||||
return [0, 0.0];
|
||||
}
|
||||
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(trip_miles), 0) FROM trips LEFT JOIN clients ON client_id = trip_client_id
|
||||
WHERE trips.$column = " . intval($id) . " AND trips.trip_archived_at IS NULL " . tripScopeSql()));
|
||||
return [intval($row[0]), floatval($row[1])];
|
||||
}
|
||||
|
||||
/** A small table of trips - from tripsFor() - each opening its quick look. For a site's Trips tab. */
|
||||
function tripTableHtml(array $rows) {
|
||||
$html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0 itflow-trip-table">'
|
||||
. '<thead class="text-nowrap table-light"><tr><th>Date</th><th>Trip</th><th>Driver</th><th class="text-end">' . ucfirst(tripUnit()) . '</th></tr></thead><tbody>';
|
||||
foreach ($rows as $row) {
|
||||
$open = 'href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/trip/trip.php?id=' . intval($row['trip_id']) . '"';
|
||||
$ticket = $row['ticket_number'] !== null && intval($row['trip_ticket_id']) ? '<span class="font-monospace text-body-secondary me-1">' . escapeHtml(ticketReference($row)) . '</span>' : '';
|
||||
$html .= '<tr>'
|
||||
. '<td class="text-nowrap"><a ' . $open . '>' . escapeHtml(date('M j, Y', strtotime($row['trip_date']))) . '</a></td>'
|
||||
. '<td class="itflow-trip-route"><div class="text-truncate">' . escapeHtml($row['trip_source']) . ' → ' . escapeHtml($row['trip_destination']) . '</div>'
|
||||
. '<div class="small text-body-secondary text-truncate">' . $ticket . escapeHtml($row['trip_purpose']) . '</div></td>'
|
||||
. '<td class="text-nowrap">' . formatAssignee($row['trip_user_id'], $row['driver_name'] ?? '', $row['driver_avatar'] ?? '', false, 'No driver') . '</td>'
|
||||
. '<td class="text-end text-nowrap">' . escapeHtml(tripMilesText($row['trip_miles']))
|
||||
. (intval($row['round_trip']) ? '<i class="fas fa-sync-alt text-body-secondary ms-1" title="Round trip"></i>' : '') . '</td>'
|
||||
. '</tr>';
|
||||
}
|
||||
return $html . '</tbody></table></div>';
|
||||
}
|
||||
Reference in New Issue
Block a user