Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions _ide_helper_actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ class FmcsaNameLookup
class GetCarrierAuditHistory
{
}
/**
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Carriers\Carrier $carrier)
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Carriers\Carrier $carrier)
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Carriers\Carrier $carrier)
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Carriers\Carrier $carrier)
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Carriers\Carrier $carrier)
* @method static dispatchSync(\App\Models\Carriers\Carrier $carrier)
* @method static dispatchNow(\App\Models\Carriers\Carrier $carrier)
* @method static dispatchAfterResponse(\App\Models\Carriers\Carrier $carrier)
* @method static ?\App\Models\Carriers\CarrierSaferReport run(\App\Models\Carriers\Carrier $carrier)
*/
class RefreshCarrierSaferReport
{
}
/**
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Carriers\Carrier $carrier, ?string $name = null, ?string $mc_number = null, ?string $dot_number = null, ?int $physical_location_id = null, ?string $contact_email = null, ?string $contact_phone = null)
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Carriers\Carrier $carrier, ?string $name = null, ?string $mc_number = null, ?string $dot_number = null, ?int $physical_location_id = null, ?string $contact_email = null, ?string $contact_phone = null)
Expand Down
57 changes: 57 additions & 0 deletions app/Actions/Carriers/RefreshCarrierSaferReport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\Actions\Carriers;

use App\Models\Carriers\Carrier;
use App\Models\Carriers\CarrierSaferReport;
use Illuminate\Support\Facades\Gate;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;

class RefreshCarrierSaferReport
{
use AsAction;

public function handle(Carrier $carrier): ?CarrierSaferReport
{
// Check if carrier has a DOT number
if (!$carrier->dot_number) {
throw new \Exception('Carrier does not have a DOT number');
}

// Check if 24 hours have passed since last update
if ($carrier->safer_report) {
$lastUpdated = new \DateTime($carrier->safer_report->updated_at);
$now = new \DateTime();
$hoursSinceUpdate = ($now->getTimestamp() - $lastUpdated->getTimestamp()) / 3600;

if ($hoursSinceUpdate < 24) {
throw new \Exception('SAFER data can only be refreshed once every 24 hours');
}
}

// Fetch new SAFER data
$newReport = app(FmcsaDOTLookup::class)->handle($carrier->dot_number);

if ($newReport) {
// Touch the updated_at timestamp
$newReport->touch();
}

return $newReport;
}

public function asController(Carrier $carrier, ActionRequest $request)
{
Gate::authorize(\App\Enums\Permission::CARRIER_EDIT);

$this->handle($carrier);

return redirect()->back()->with('success', 'SAFER data refreshed successfully');
}

public function authorize(ActionRequest $request): bool
{
return $request->user()->can(\App\Enums\Permission::CARRIER_EDIT->value);
}
}
78 changes: 45 additions & 33 deletions app/Actions/Organizations/CheckShipmentLimits.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,16 @@ class CheckShipmentLimits

public function handle(Organization $organization): void
{
// Only check load limits for startup subscriptions
if (!$organization->subscription(SubscriptionType::STARTUP->value)) {
return;
}

$subscription = $organization->subscription(SubscriptionType::STARTUP->value);

if (!in_array($subscription->stripe_status, ['active', 'trialing'])) {

if (!$subscription || !in_array($subscription->stripe_status, ['active', 'trialing'])
|| $this->billingDisabled()
|| $this->hasActivePremiumSubscription($organization)) {
return;
}

$weeklyLimit = config('subscriptions.startup.weekly_load_limit', 10);

// Count shipments created this week
$startOfWeek = now()->startOfWeek();
$endOfWeek = now()->endOfWeek();

Expand All @@ -48,35 +44,51 @@ public function handle(Organization $organization): void
*/
public function getShipmentUsage(Organization $organization): array
{
// Check for premium subscription first (USER_SEAT has unlimited shipments)
if ($organization->subscribed(SubscriptionType::USER_SEAT->value)) {
$subscription = $organization->subscription(SubscriptionType::USER_SEAT->value);

if ($subscription && in_array($subscription->stripe_status, ['active', 'trialing'])) {
// Premium users have unlimited shipments
return [
'is_startup_plan' => false,
'weekly_limit' => null, // null indicates unlimited
'shipments_this_week' => 0, // Not relevant for premium
'remaining_shipments' => null, // null indicates unlimited
'week_start' => null,
'week_end' => null,
];
}
// If billing is disabled, return unlimited usage
if ($this->billingDisabled()
|| $this->hasActivePremiumSubscription($organization)) {
return $this->getUnlimitedUsage();
}

// Check startup subscription limits
return $this->getStartupUsage($organization);
}

private function getUnlimitedUsage(): array
{
return [
'is_startup_plan' => false,
'weekly_limit' => null, // null indicates unlimited
'shipments_this_week' => 0, // Not relevant when unlimited
'remaining_shipments' => null, // null indicates unlimited
'week_start' => null,
'week_end' => null,
];
}

private function billingDisabled(): bool
{
return !config('cashier.key') || config('subscriptions.enable_billing') === false;
}

private function hasActivePremiumSubscription(Organization $organization): bool
{
if (!$organization->subscribed(SubscriptionType::USER_SEAT->value)) {
return false;
}

// Handle startup subscription only if no premium subscription exists
$subscription = $organization->subscription(SubscriptionType::USER_SEAT->value);

return $subscription && in_array($subscription->stripe_status, ['active', 'trialing']);
}

private function getStartupUsage(Organization $organization): array
{
$subscription = $organization->subscription(SubscriptionType::STARTUP->value);

if (!in_array($subscription->stripe_status, ['active', 'trialing'])) {
return [
'is_startup_plan' => false,
'weekly_limit' => 0,
'shipments_this_week' => 0,
'remaining_shipments' => 0,
'week_start' => null,
'week_end' => null,
];
// If no startup subscription or not active, treat as unlimited
if (!$subscription || !in_array($subscription->stripe_status, ['active', 'trialing'])) {
return $this->getUnlimitedUsage();
}

$weeklyLimit = config('subscriptions.startup.weekly_load_limit', 10);
Expand Down
13 changes: 13 additions & 0 deletions app/Http/Controllers/CarrierController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ public function create()
public function show(Carrier $carrier)
{
Gate::authorize(\App\Enums\Permission::CARRIER_VIEW);

// If carrier has DOT number but no SAFER report, try to fetch it
if ($carrier->dot_number && !$carrier->safer_report && config('fmcsa.api_key')) {
try {
app(\App\Actions\Carriers\FmcsaDOTLookup::class)->handle($carrier->dot_number);
// Reload the carrier to get the newly created safer_report
$carrier->load('safer_report');
} catch (\Exception $e) {
// Log the error but don't fail the page load
\Log::error('Failed to fetch SAFER report for carrier ' . $carrier->id . ': ' . $e->getMessage());
}
}

return Inertia::render('Carriers/Show', [
'carrier' => CarrierResource::make($carrier->load('physical_location', 'billing_location', 'safer_report')),
]);
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class Location extends Model
'longitude',
];

protected $casts = [
'latitude' => 'float',
'longitude' => 'float',
];

protected $appends = [ 'selectable_label' ];

public function getSelectableLabelAttribute() : string
Expand Down
10 changes: 7 additions & 3 deletions app/Traits/HandlesAuditHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ private function getRelatedModelAudits(
$audits = collect();

// Get audits for models that currently exist and belong to this parent
// We need to join with the actual model table to filter by the polymorphic relationship
$modelTable = (new $auditableType)->getTable();
$existingModelAudits = Audit::where('auditable_type', $auditableType)
->whereHas('auditable', function ($subQuery) use ($parentClass, $parentId, $typeField, $idField) {
$subQuery->where($typeField, $parentClass)
->where($idField, $parentId);
->join($modelTable, function ($join) use ($modelTable) {
$join->on('audits.auditable_id', '=', $modelTable . '.id');
})
->where($modelTable . '.' . $typeField, $parentClass)
->where($modelTable . '.' . $idField, $parentId)
->select('audits.*')
->with('user', 'auditable')
->get();

Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ services:
networks:
- sail
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
image: localstack/localstack
profiles:
- full
Expand Down
7 changes: 7 additions & 0 deletions resources/js/Components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
useSidebar,
} from '@/Components/ui/sidebar';
import { usePage } from '@inertiajs/react';
import {
Expand All @@ -41,6 +42,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const config = usePage().props.config;
const [isOrgMenuOpen, setIsOrgMenuOpen] = useState(false);
const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false);
const { state, setOpen } = useSidebar();

useEffect(() => {
const savedState = localStorage.getItem('orgMenuOpen');
Expand All @@ -52,6 +54,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const handleOrgMenuChange = (open: boolean) => {
setIsOrgMenuOpen(open);
localStorage.setItem('orgMenuOpen', open.toString());

// If opening the organization menu while sidebar is collapsed, expand the sidebar
if (open && state === 'collapsed') {
setOpen(true);
}
};

// Early return if user is not loaded yet
Expand Down
5 changes: 3 additions & 2 deletions resources/js/Components/Audit/AuditFieldValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export default function AuditFieldValue({

const formatValue = (value: unknown): string => {
// Check for folder-related fields (both raw and formatted names)
const isFolderField = fieldName === 'folder_name' || fieldName === 'Folder Name';
const isFolderField =
fieldName === 'folder_name' || fieldName === 'Folder Name';
const isPathField = fieldName === 'path' || fieldName === 'Path';

if (value === null || value === undefined) {
// For path fields, only show '/' if the value was explicitly set to null/undefined
// but not when there was no previous value (which should show 'No value')
Expand Down
11 changes: 10 additions & 1 deletion resources/js/Components/Shipments/LocationMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,16 @@ export default function LocationMap({ shipment }: { shipment: Shipment }) {
<Marker
key={index}
position={{ lat: marker.lat, lng: marker.lng }}
label={{ text: marker.label, color: 'white' }}
icon={{
url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="12" fill="${marker.stopType === 'pickup' ? '#ef4444' : '#3b82f6'}" stroke="white" stroke-width="2"/>
<text x="16" y="20" text-anchor="middle" fill="white" font-family="Arial" font-size="12" font-weight="bold">${marker.label}</text>
</svg>
`)}`,
scaledSize: new google.maps.Size(32, 32),
anchor: new google.maps.Point(16, 16),
}}
/>
))}
</GoogleMap>
Expand Down
2 changes: 1 addition & 1 deletion resources/js/Components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-base text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
)}
ref={ref}
Expand Down
Loading