diff --git a/.gitignore b/.gitignore
index 2e7256eadb..20c803a683 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
/public/storage
/public/uploads
/public/devices.csv
+/public/exports
/public/report.html
npm-debug.log
yarn-error.log
diff --git a/app/Console/Commands/ExportRepairData.php b/app/Console/Commands/ExportRepairData.php
new file mode 100644
index 0000000000..c4a9f60051
--- /dev/null
+++ b/app/Console/Commands/ExportRepairData.php
@@ -0,0 +1,88 @@
+count(),
+ DB::table('devices')->max('updated_at'),
+ DB::table('events')->max('updated_at'),
+ DB::table('groups')->max('updated_at'),
+ ]);
+ }
+
+ public function handle(RepairDataExport $export): int
+ {
+ $path = self::path();
+ $fingerprint = $this->fingerprint();
+
+ if (! $this->option('force') && file_exists($path)) {
+ if (time() - filemtime($path) < self::MAX_AGE_SECONDS) {
+ $this->info('Built recently; not rebuilding.');
+
+ return self::SUCCESS;
+ }
+
+ if (@file_get_contents($this->fingerprintPath()) === $fingerprint) {
+ // Mark it as checked, so we don't check again until tomorrow.
+ touch($path);
+ $this->info('Data unchanged; not rebuilding.');
+
+ return self::SUCCESS;
+ }
+ }
+
+ foreach ([dirname($path), dirname($this->fingerprintPath())] as $dir) {
+ if (! is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+ }
+
+ // Build alongside and then swap in, so nobody downloads a half-written file.
+ $tmp = $path.'.tmp';
+ $file = fopen($tmp, 'w');
+ $export->write($export->query(), $file);
+ fclose($file);
+ rename($tmp, $path);
+
+ file_put_contents($this->fingerprintPath(), $fingerprint);
+ $this->info('Built '.$path);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index a33477ed8d..2fe0c8593a 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -44,6 +44,9 @@ protected function schedule(Schedule $schedule): void
$schedule->command('event:timezones')->hourly();
$schedule->command('wordpress:event:create_failed')->daily();
+
+ // Rebuilds at most daily; hourly so that a new machine gets the file soon after it starts.
+ $schedule->command('export:repair-data')->hourly()->withoutOverlapping();
}
/**
diff --git a/app/Device.php b/app/Device.php
index 627cf20463..200cd7510c 100644
--- a/app/Device.php
+++ b/app/Device.php
@@ -207,6 +207,9 @@ public function countByClustersYearStatus($group)
return DB::select($sql, ['group' => $group]);
}
+ /**
+ * The most common categories, powered and unpowered, leaving out the "None of the above" ones.
+ */
public function findMostSeen($status = null, $cluster = null, $group = null)
{
$sql = 'SELECT COUNT(`d`.`category`) AS `counter`, `c`.`name` FROM `'.$this->table.'` AS `d`
@@ -214,7 +217,7 @@ public function findMostSeen($status = null, $cluster = null, $group = null)
ON `d`.`event` = `e`.`idevents`
INNER JOIN `categories` AS `c`
ON `d`.`category` = `c`.`idcategories`
- WHERE 1=1 and `c`.`powered` = 1 AND `c`.`idcategories` <> '.env('MISC_CATEGORY_ID_POWERED');
+ WHERE `c`.`idcategories` NOT IN ('.(int) env('MISC_CATEGORY_ID_POWERED').', '.(int) env('MISC_CATEGORY_ID_UNPOWERED').')';
if (! is_null($status) && is_numeric($status)) {
$sql .= ' AND `d`.`repair_status` = :status ';
diff --git a/app/Group.php b/app/Group.php
index 097de77cf5..34fd9cd7f7 100644
--- a/app/Group.php
+++ b/app/Group.php
@@ -497,6 +497,35 @@ public function isMemberOf($network)
return $this->networks->contains($network);
}
+ /**
+ * The group reporting dashboard, filtered to this group, or null if reporting isn't set up.
+ *
+ * The dashboard is in the language of the group's network, falling back to English when there's no
+ * dashboard in that language or its base language.
+ */
+ public function reportingUrl(): ?string
+ {
+ $templates = config('restarters.reporting.group_urls', []);
+ $network = $this->networks->first();
+ $language = $network ? $network->default_language : null;
+
+ // A regional language (e.g. Repair Together's fr-BE) uses the dashboard for its base language.
+ $base = $language ? explode('-', $language)[0] : null;
+ $template = ($language ? ($templates[$language] ?? null) : null)
+ ?: ($base ? ($templates[$base] ?? null) : null)
+ ?: ($templates['en'] ?? null);
+
+ if (! $template) {
+ return null;
+ }
+
+ return str_replace(
+ ['{group}', '{group_name}'],
+ [$this->idgroups, rawurlencode($this->name)],
+ $template
+ );
+ }
+
// If just one of the networks that the group is a member of should push to Wordpress, then we should push.
// If an group is not approved, then we should not push the events to Wordpress.
public function eventsShouldPushToWordpress()
diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php
index 7e6f2e05da..878694a4a9 100644
--- a/app/Http/Controllers/ApiController.php
+++ b/app/Http/Controllers/ApiController.php
@@ -7,6 +7,7 @@
use App\Group;
use App\Party;
use App\User;
+use Carbon\Carbon;
use Auth;
use DB;
use Illuminate\Http\Request;
@@ -51,24 +52,27 @@ public static function homepage_data(): JsonResponse
{
$result = [];
- $lock = \Cache::lock('homepage_data_lock', 60);
+ $lock = \Cache::lock('homepage_data_v3_lock', 60);
- if (\Cache::has('homepage_data')) {
- $result = \Cache::get('homepage_data');
+ if (\Cache::has('homepage_data_v3')) {
+ $result = \Cache::get('homepage_data_v3');
} elseif ($lock->get()) {
try {
$Device = new Device;
// Aggregate participants and hours in SQL — avoids loading 18k+ event rows into PHP.
// hoursVolunteered() formula: cancelled→3, volunteers>0→9+volunteers*ceil(minutes/60), else→21
+ // Events held leaves out cancelled events and those of groups that aren't approved.
$eventStats = DB::table('events')
- ->whereNull('deleted_at')
- ->where('event_end_utc', '<', now())
+ ->leftJoin('groups', 'groups.idgroups', '=', 'events.group')
+ ->whereNull('events.deleted_at')
+ ->where('events.event_end_utc', '<', now())
->selectRaw("
- SUM(pax) as participants,
+ SUM(CASE WHEN events.cancelled = 0 AND groups.approved = 1 THEN 1 ELSE 0 END) as events,
+ SUM(events.pax) as participants,
SUM(CASE
- WHEN cancelled = 1 THEN 3
- WHEN volunteers > 0 THEN 9 + volunteers * CEIL(TIMESTAMPDIFF(MINUTE, event_start_utc, event_end_utc) / 60)
+ WHEN events.cancelled = 1 THEN 3
+ WHEN events.volunteers > 0 THEN 9 + events.volunteers * CEIL(TIMESTAMPDIFF(MINUTE, events.event_start_utc, events.event_end_utc) / 60)
ELSE 21
END) as hours_volunteered
")
@@ -76,6 +80,7 @@ public static function homepage_data(): JsonResponse
$result['participants'] = (int) ($eventStats->participants ?? 0);
$result['hours_volunteered'] = (int) ($eventStats->hours_volunteered ?? 0);
+ $result['events'] = (int) ($eventStats->events ?? 0);
$fixed = $Device->statusCount();
$result['items_fixed'] = count($fixed) ? $fixed[0]->counter : 0;
@@ -93,6 +98,7 @@ public static function homepage_data(): JsonResponse
$result['fixed_unpowered'] = $devices->fixedUnpoweredCount();
$result['total_powered'] = $devices->poweredCount();
$result['total_unpowered'] = $devices->unpoweredCount();
+ $result['total_items'] = $result['total_powered'] + $result['total_unpowered'];
// for backward compatibility (don't break therestartproject.org)
$result['weights'] = round($result['waste_total']);
@@ -100,13 +106,13 @@ public static function homepage_data(): JsonResponse
$result['unpowered_waste'] = round($result['waste_unpowered']);
$result['emissions'] = round($result['co2_total']);
- \Cache::put('homepage_data', $result, 43200);
+ \Cache::put('homepage_data_v3', $result, 43200);
} finally {
$lock->release();
}
} else {
// Another worker is rebuilding — return stale or empty rather than pile on
- $result = \Cache::get('homepage_data', []);
+ $result = \Cache::get('homepage_data_v3', []);
}
return response()
@@ -200,75 +206,107 @@ public static function getUserList()
}
/**
- * List/search devices.
+ * The where clauses for the device list's search filters.
*/
- public static function getDevices(Request $request, $page, $size): JsonResponse
+ private static function deviceFilters(Request $request): array
{
- $powered = $request->input('powered');
- $sortBy = $request->input('sortBy');
- $sortDesc = $request->input('sortDesc');
- $category = $request->input('category');
- $brand = $request->input('brand');
- $model = $request->input('model');
- $item_type = $request->input('item_type');
- $status = $request->input('status');
- $comments = $request->input('comments');
- $wiki = filter_var($request->input('wiki', false), FILTER_VALIDATE_BOOLEAN);
- $group = $request->input('group');
- $from_date = $request->input('from_date');
- $to_date = $request->input('to_date');
-
- $wheres = [
- ['categories.powered', '=', $powered == 'true' ? 1 : 0],
- ];
+ $wheres = [];
- if ($category) {
- $wheres[] = ['idcategories', '=', $category];
- }
+ // No powered filter means both powered and unpowered items.
+ $powered = $request->input('powered');
- if ($brand) {
- $wheres[] = ['devices.brand', 'LIKE', '%'.$brand.'%'];
+ if ($powered === 'true' || $powered === 'false') {
+ $wheres[] = ['categories.powered', '=', $powered === 'true' ? 1 : 0];
}
- if ($model) {
- $wheres[] = ['devices.model', 'LIKE', '%'.$model.'%'];
+ if ($request->input('category')) {
+ $wheres[] = ['idcategories', '=', $request->input('category')];
}
- if ($item_type) {
- $wheres[] = ['devices.item_type', 'LIKE', '%'.$item_type.'%'];
- }
+ // Free text searches.
+ $likes = [
+ 'brand' => 'devices.brand',
+ 'model' => 'devices.model',
+ 'item_type' => 'devices.item_type',
+ 'comments' => 'devices.problem',
+ 'group' => 'groups.name',
+ ];
- if ($comments) {
- $wheres[] = ['devices.problem', 'LIKE', '%'.$comments.'%'];
+ foreach ($likes as $param => $column) {
+ if ($request->input($param)) {
+ $wheres[] = [$column, 'LIKE', '%'.$request->input($param).'%'];
+ }
}
- if ($wiki) {
+ if (filter_var($request->input('wiki', false), FILTER_VALIDATE_BOOLEAN)) {
$wheres[] = ['devices.wiki', '=', 1];
}
+ $status = $request->input('status');
+
if ($status) {
- $wheres[] = ['repair_status', '=', $status];
- }
+ // The client uses the status strings from the rest of the API; accept the underlying numbers too.
+ $statuses = [
+ Device::REPAIR_STATUS_FIXED_STR => Device::REPAIR_STATUS_FIXED,
+ Device::REPAIR_STATUS_REPAIRABLE_STR => Device::REPAIR_STATUS_REPAIRABLE,
+ Device::REPAIR_STATUS_ENDOFLIFE_STR => Device::REPAIR_STATUS_ENDOFLIFE,
+ ];
- if ($group) {
- $wheres[] = ['groups.name', 'LIKE', '%'.$group.'%'];
+ $wheres[] = ['repair_status', '=', $statuses[$status] ?? intval($status)];
}
- if ($from_date) {
- $wheres[] = ['events.event_start_utc', '>=', $from_date];
+ if ($request->input('from_date')) {
+ $wheres[] = ['events.event_start_utc', '>=', Carbon::parse($request->input('from_date'))->startOfDay()];
}
- if ($to_date) {
- $wheres[] = ['events.event_end_utc', '<=', $to_date];
+ if ($request->input('to_date')) {
+ // The date is inclusive - events on that day count.
+ $wheres[] = ['events.event_start_utc', '<', Carbon::parse($request->input('to_date'))->startOfDay()->addDay()];
}
- // Get the items we want for this page.
+ return $wheres;
+ }
+
+ /**
+ * List/search devices.
+ */
+ public static function getDevices(Request $request, $page, $size): JsonResponse
+ {
+ $request->validate([
+ 'from_date' => 'nullable|date',
+ 'to_date' => 'nullable|date',
+ ]);
+
+ // The client asks to sort by one of its table columns; map those onto database columns. Anything else
+ // gets the default of most recent event first.
+ $sortColumns = [
+ 'item_type' => 'devices.item_type',
+ 'category' => 'categories.name',
+ 'device_category.name' => 'categories.name',
+ 'brand' => 'devices.brand',
+ 'groupname' => 'groups.name',
+ 'repair_status' => 'devices.repair_status',
+ 'event_date' => 'events.event_start_utc',
+ 'created_at' => 'devices.created_at',
+ 'iddevices' => 'devices.iddevices',
+ ];
+
+ $sortBy = $sortColumns[$request->input('sortBy')] ?? 'events.event_start_utc';
+ $sortDesc = strtolower($request->input('sortDesc', 'desc')) === 'asc' ? 'asc' : 'desc';
+
+ $wheres = self::deviceFilters($request);
+
+ // Get the items we want for this page. Select only device columns - the joined tables share column names
+ // such as created_at, which would otherwise overwrite the device's own.
$query = Device::with(['deviceEvent.theGroup', 'deviceCategory', 'barriers'])
+ ->select('devices.*')
->join('events', 'events.idevents', '=', 'devices.event')
->join('groups', 'events.group', '=', 'groups.idgroups')
->join('categories', 'devices.category', '=', 'categories.idcategories')
+ ->whereNull('events.deleted_at')
->where($wheres)
- ->orderBy($sortBy, $sortDesc);
+ ->orderBy($sortBy, $sortDesc)
+ ->orderBy('devices.iddevices', $sortDesc);
// Get total info across all pages.
$count = $query->count();
diff --git a/app/Http/Controllers/DeviceController.php b/app/Http/Controllers/DeviceController.php
index f68d119897..494906b8ec 100644
--- a/app/Http/Controllers/DeviceController.php
+++ b/app/Http/Controllers/DeviceController.php
@@ -62,6 +62,8 @@ public function index($search = null): \Illuminate\View\View
'clusters' => $clusters,
'barriers' => \App\Helpers\Fixometer::allBarriers(),
'brands' => $brands,
+ // The nightly file if it's there; a new machine won't have it until the hourly build has run.
+ 'download_url' => file_exists(\App\Console\Commands\ExportRepairData::path()) ? '/exports/repair-data.csv' : '/export/devices',
]);
}
diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php
index 2a48b2dbfd..0116a82734 100644
--- a/app/Http/Controllers/ExportController.php
+++ b/app/Http/Controllers/ExportController.php
@@ -37,23 +37,10 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL)
// To not display column if the referring URL is therestartproject.org
$host = parse_url(\Request::server('HTTP_REFERER'), PHP_URL_HOST);
- $all_devices = Device::with([
- 'deviceCategory',
- 'deviceEvent',
- ])
- ->join('events', 'events.idevents', '=', 'devices.event')
- ->join('groups', 'groups.idgroups', '=', 'events.group')
- ->when($idevents != NULL, function($query) use ($idevents) {
- return $query->where('events.idevents', $idevents);
- })
- ->when($idgroups != NULL, function($query) use ($idgroups) {
- return $query->where('events.group', $idgroups);
- })
- ->select('devices.*', 'groups.name AS group_name')->get();
+ $me = auth()->user();
- $displacementFactor = \App\Device::getDisplacementFactor();
- $eEmissionRatio = \App\Helpers\LcaStats::getEmissionRatioPowered();
- $uEmissionratio = \App\Helpers\LcaStats::getEmissionRatioUnpowered();
+ $export = app(\App\Services\RepairDataExport::class);
+ $all_devices = $export->query($me, $idevents, $idgroups);
// Create CSV
$filename = 'repair-data';
@@ -75,70 +62,7 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL)
$fullpath = $this->exportPath($filename);
$file = fopen($fullpath, 'w+');
- $me = auth()->user();
-
- // We can't put accented characters into a CSV file, so flatten them.
- // Use //TRANSLIT//IGNORE to handle characters that can't be transliterated on
- // servers with older glibc (e.g. 2.27) and POSIX locale, which lack transliteration
- // tables for certain Unicode characters like emdash (—). Without //IGNORE, iconv
- // throws "Detected an illegal character in input string" on such systems.
- $columns = [
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.item_type_short')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.category')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.brand')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.model')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.title_assessment')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.repair_status')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.spare_parts')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.event')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('groups.group')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.event_date')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.stat-7')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.stat-6')),
- iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', ucfirst(__('devices.title_powered')))
- ];
-
- fputcsv($file, $columns);
- $party = null;
-
- foreach ($all_devices as $device) {
- set_time_limit(60);
- $party = !$party || $party->idevents != $device->event ? Party::findOrFail($device->event) : $party;
-
- if (User::userCanSeeEvent($me, $party)) {
- $wasteImpact = 0;
- $co2Diverted = 0;
-
- if ($device->isFixed())
- {
- if ($device->deviceCategory->powered)
- {
- $wasteImpact = $device->eWasteDiverted();
- $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor);
- } else
- {
- $wasteImpact = $device->uWasteDiverted();
- $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor);
- }
- }
-
- fputcsv($file, $this->csvSafeRow([
- $device->item_type,
- $device->deviceCategory->name,
- $device->brand,
- $device->model,
- $device->problem,
- $device->getRepairStatus(),
- $device->getSpareParts(),
- $device->deviceEvent->getEventName(),
- $device->deviceEvent->theGroup->name,
- $device->deviceEvent->getFormattedLocalStart('Y-m-d'),
- $wasteImpact,
- $co2Diverted,
- $device->deviceCategory->powered ? 'Powered' : 'Unpowered'
- ]));
- }
- }
+ $export->write($all_devices, $file);
fclose($file);
@@ -226,7 +150,7 @@ private function exportEvents($parties) {
fputcsv($file, $headers);
foreach ($PartyArray as $d) {
- fputcsv($file, $this->csvSafeRow($d));
+ fputcsv($file, \App\Services\RepairDataExport::csvSafeRow($d));
}
fclose($file);
@@ -252,19 +176,4 @@ private function exportPath($filename)
return $dir . DIRECTORY_SEPARATOR . $filename;
}
-
- /**
- * Spreadsheets treat a cell starting with =, +, - or @ as a formula, so prefix those
- * with an apostrophe. Device fields are free text entered at events.
- */
- private function csvSafeRow(array $row)
- {
- return array_map(function ($value) {
- if (is_string($value) && $value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
- return "'" . $value;
- }
-
- return $value;
- }, $row);
- }
}
diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php
index a28eaabcba..e95347c92b 100644
--- a/app/Http/Controllers/GroupController.php
+++ b/app/Http/Controllers/GroupController.php
@@ -192,66 +192,6 @@ public function view($groupid)
$Device->ofThisGroup($group->idgroups);
- $counts = $Device->countByClustersYearStatus($group->idgroups);
- $template = [
- 0 => [
- 'counter' => 0,
- 'repair_status' => 1,
- ],
- 1 => [
- 'counter' => 0,
- 'repair_status' => 2,
- ],
- 2 => [
- 'counter' => 0,
- 'repair_status' => 3,
- ],
- 'total' => 0
- ];
-
- $clusters = [
- 'all' => [
- 1 => $template,
- 2 => $template,
- 3 => $template,
- 4 => $template,
- ]
- ];
-
- foreach ($counts as $count) {
- $year = $count->year;
- $cluster = $count->cluster;
- $repair_status = $count->repair_status;
- $counter = $count->counter;
-
- if ($repair_status && $cluster) {
- if (array_key_exists($cluster, $clusters['all'])) {
- $clusters['all'][$cluster][$repair_status - 1]['counter'] += $counter;
- $clusters['all'][$cluster]['total'] += $counter;
-
- if (!array_key_exists($year, $clusters)) {
- $clusters[$year] = [
- 1 => $template,
- 2 => $template,
- 3 => $template,
- 4 => $template,
- ];
- }
-
- $clusters[$year][$cluster][$repair_status - 1]['counter'] += $counter;
- $clusters[$year][$cluster]['total'] += $counter;
- }
- }
- }
-
- // most/least stats for clusters
- $mostleast = [];
- for ($i = 1; $i <= 4; $i++) {
- $mostleast[$i]['most_seen'] = $Device->findMostSeen(null, $i, $group->idgroups);
- $mostleast[$i]['most_repaired'] = $Device->findMostSeen(1, $i, $group->idgroups);
- $mostleast[$i]['least_repaired'] = $Device->findMostSeen(3, $i, $group->idgroups);
- }
-
if (! isset($response)) {
$response = null;
}
@@ -319,8 +259,6 @@ public function view($groupid)
'group_device_count_status' => $Device->statusCount($group->idgroups),
'group_stats' => $groupStats,
'expanded_events' => $expanded_events,
- 'clusters' => $clusters,
- 'mostleast' => $mostleast,
'top' => $Device->findMostSeen(1, null, $group->idgroups),
'user' => $user,
'upcoming_events' => $upcoming_events,
diff --git a/app/Http/Controllers/NetworkController.php b/app/Http/Controllers/NetworkController.php
index ef269b8022..018fc4f87d 100644
--- a/app/Http/Controllers/NetworkController.php
+++ b/app/Http/Controllers/NetworkController.php
@@ -87,6 +87,7 @@ public function show(Network $network): View
'name' => $network->name,
'description' => $network->description,
'website' => $network->website,
+ 'reporting_url' => $network->reporting_url,
'logo' => $network->sizedLogo('_x100'),
'coordinators' => $network->coordinators->map(function ($c) {
$profile = $c->getProfile($c->id);
@@ -120,6 +121,7 @@ public function edit(Network $network): View
return view('networks.edit', [
'network' => $network,
+ 'canEditReportingUrl' => Fixometer::hasRole(Auth::user(), 'Administrator'),
]);
}
@@ -130,6 +132,16 @@ public function update(Request $request, Network $network): RedirectResponse
{
$this->authorize('update', $network);
+ // The reporting dashboard address is set up by administrators alongside the dashboard itself.
+ if (Fixometer::hasRole(Auth::user(), 'Administrator') && $request->has('reporting_url')) {
+ $request->validate([
+ 'reporting_url' => 'nullable|url:https,http|max:2048',
+ ]);
+
+ $network->reporting_url = $request->input('reporting_url') ?: null;
+ $network->save();
+ }
+
if ($request->hasFile('network_logo')) {
if (! config('restarters.features.image_upload')) {
return redirect()->route('networks.edit', [$network])
diff --git a/app/Http/Resources/Device.php b/app/Http/Resources/Device.php
index f2ed2806e3..9a313f7e92 100644
--- a/app/Http/Resources/Device.php
+++ b/app/Http/Resources/Device.php
@@ -4,6 +4,7 @@
use Illuminate\Http\Request;
use App\Party;
+use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
/**
@@ -34,6 +35,13 @@
* example="Europe/London"
* ),
* @OA\Property(
+ * property="event_date",
+ * title="event_date",
+ * description="When the event started. Provided for convenience to avoid extra API calls.",
+ * format="date-time",
+ * example="2024-03-10T13:00:00+00:00"
+ * ),
+ * @OA\Property(
* property="groupid",
* title="groupid",
* description="The group to which this device belongs.",
@@ -195,6 +203,7 @@ public function toArray(Request $request): array
'id' => intval($this->iddevices),
'eventid' => intval($this->event),
'eventtitle' => $event ? $event->title : NULL,
+ 'event_date' => $event ? Carbon::parse($event->event_start_utc)->toIso8601String() : null,
'groupid' => $event ? intval($event->group) : NULL,
'groupname' => $group ? $group->name : NULL,
'category' => intval($this->category),
diff --git a/app/Services/RepairDataExport.php b/app/Services/RepairDataExport.php
new file mode 100644
index 0000000000..2b1d37f911
--- /dev/null
+++ b/app/Services/RepairDataExport.php
@@ -0,0 +1,158 @@
+join('events', 'events.idevents', '=', 'devices.event')
+ ->join('groups', 'groups.idgroups', '=', 'events.group')
+ ->whereNull('events.deleted_at')
+ ->when($idevents != null, function ($query) use ($idevents) {
+ return $query->where('events.idevents', $idevents);
+ })
+ ->when($idgroups != null, function ($query) use ($idgroups) {
+ return $query->where('events.group', $idgroups);
+ })
+ ->when(! $viewer || ! $viewer->hasRole('Administrator'), function ($query) use ($viewer) {
+ $extraGroups = $this->groupsWithUnapprovedEventsVisibleTo($viewer);
+
+ return $query->where(function ($query) use ($extraGroups) {
+ $query->where(function ($query) {
+ $query->where('events.approved', true)
+ ->where('groups.approved', true);
+ })->orWhereIn('events.group', $extraGroups);
+ });
+ })
+ ->select('devices.*', 'groups.name AS group_name');
+ }
+
+ /**
+ * Write the devices a query finds to an open file.
+ */
+ public function write($devices, $file): void
+ {
+ $displacementFactor = Device::getDisplacementFactor();
+ $eEmissionRatio = \App\Helpers\LcaStats::getEmissionRatioPowered();
+ $uEmissionratio = \App\Helpers\LcaStats::getEmissionRatioUnpowered();
+
+ // We can't put accented characters into a CSV file, so flatten them.
+ // Use //TRANSLIT//IGNORE to handle characters that can't be transliterated on
+ // servers with older glibc (e.g. 2.27) and POSIX locale, which lack transliteration
+ // tables for certain Unicode characters like emdash (—). Without //IGNORE, iconv
+ // throws "Detected an illegal character in input string" on such systems.
+ $columns = [
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.item_type_short')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.category')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.brand')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.model')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.title_assessment')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.repair_status')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('devices.spare_parts')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.event')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('groups.group')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.event_date')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.stat-7')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', __('events.stat-6')),
+ iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', ucfirst(__('devices.title_powered'))),
+ ];
+
+ fputcsv($file, $columns);
+
+ // Work through the devices in chunks so that the whole dataset isn't held in memory at once.
+ foreach ($devices->lazyById(1000, 'devices.iddevices', 'iddevices') as $device) {
+ set_time_limit(60);
+
+ $wasteImpact = 0;
+ $co2Diverted = 0;
+
+ if ($device->isFixed()) {
+ if ($device->deviceCategory->powered) {
+ $wasteImpact = $device->eWasteDiverted();
+ $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor);
+ } else {
+ $wasteImpact = $device->uWasteDiverted();
+ $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor);
+ }
+ }
+
+ fputcsv($file, self::csvSafeRow([
+ $device->item_type,
+ $device->deviceCategory->name,
+ $device->brand,
+ $device->model,
+ $device->problem,
+ $device->getRepairStatus(),
+ $device->getSpareParts(),
+ $device->deviceEvent->getEventName(),
+ $device->deviceEvent->theGroup->name,
+ $device->deviceEvent->getFormattedLocalStart('Y-m-d'),
+ $wasteImpact,
+ $co2Diverted,
+ $device->deviceCategory->powered ? 'Powered' : 'Unpowered',
+ ]));
+ }
+ }
+
+ /**
+ * Groups whose events a user can see even when the event or group isn't approved: those they host, and those in
+ * networks they coordinate.
+ */
+ private function groupsWithUnapprovedEventsVisibleTo($user): array
+ {
+ if (! $user) {
+ return [];
+ }
+
+ $groups = [];
+
+ if ($user->hasRole('Host')) {
+ $groups = UserGroups::where('user', $user->id)
+ ->where('role', \App\Role::HOST)
+ ->pluck('group')
+ ->all();
+ }
+
+ $networks = $user->networks->pluck('id');
+
+ if ($networks->count()) {
+ $groups = array_merge($groups, DB::table('group_network')
+ ->whereIn('network_id', $networks)
+ ->pluck('group_id')
+ ->all());
+ }
+
+ return array_values(array_unique($groups));
+ }
+
+ /**
+ * Spreadsheets treat a cell starting with =, +, - or @ as a formula, so prefix those
+ * with an apostrophe. Device fields are free text entered at events.
+ */
+ public static function csvSafeRow(array $row)
+ {
+ return array_map(function ($value) {
+ if (is_string($value) && $value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
+ return "'".$value;
+ }
+
+ return $value;
+ }, $row);
+ }
+}
diff --git a/config/restarters.php b/config/restarters.php
index f04f2779d0..5afe4394f5 100644
--- a/config/restarters.php
+++ b/config/restarters.php
@@ -26,6 +26,20 @@
'api_key' => env('CARTO_API_KEY'),
],
+ 'reporting' => [
+ // The group reporting dashboard, filtered to one group, one per language - Metabase can only show a
+ // dashboard in another language as a separate dashboard. {group} is replaced by the group id and
+ // {group_name} by its name, e.g.
+ // https://metabase.example.org/public/dashboard/abc?group_id={group}&group_name={group_name}#hide_parameters=group_id%2Cgroup_name
+ // A group gets the dashboard for its network's language, or English if there isn't one.
+ 'group_urls' => [
+ 'en' => env('GROUP_REPORTING_URL_EN'),
+ 'fr' => env('GROUP_REPORTING_URL_FR'),
+ ],
+ // Further reports on all the repair data, linked from the Fixometer page. No link if unset.
+ 'fixometer_url' => env('FIXOMETER_REPORTING_URL'),
+ ],
+
'xref_types' => [
'networks' => 7,
],
diff --git a/database/migrations/2026_09_30_000000_add_reporting_url_to_networks_table.php b/database/migrations/2026_09_30_000000_add_reporting_url_to_networks_table.php
new file mode 100644
index 0000000000..d9de460b32
--- /dev/null
+++ b/database/migrations/2026_09_30_000000_add_reporting_url_to_networks_table.php
@@ -0,0 +1,23 @@
+string('reporting_url', 2048)->nullable();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('networks', function (Blueprint $table) {
+ $table->dropColumn('reporting_url');
+ });
+ }
+};
diff --git a/docker/nginx-fly.conf b/docker/nginx-fly.conf
index 7600041e74..70a11dfe3b 100644
--- a/docker/nginx-fly.conf
+++ b/docker/nginx-fly.conf
@@ -42,7 +42,7 @@ http {
gzip_min_length 1024;
gzip_proxied any;
gzip_vary on;
- gzip_types text/plain text/css text/javascript application/javascript application/json application/xml text/xml image/svg+xml;
+ gzip_types text/plain text/csv text/css text/javascript application/javascript application/json application/xml text/xml image/svg+xml;
server {
listen 80 default_server;
@@ -162,6 +162,13 @@ http {
location /build/ { auth_request off; expires 1y; add_header Cache-Control "public, immutable"; }
location /images/ { auth_request off; expires 7d; }
+ # The repair data download, rebuilt overnight by export:repair-data. Served as CSV so that gzip applies.
+ location /exports/ {
+ types { text/csv csv; }
+ default_type text/csv;
+ add_header Content-Disposition "attachment";
+ }
+
location = /fpm-status {
auth_request off;
allow 127.0.0.1;
diff --git a/lang/de/devices.php b/lang/de/devices.php
index 39040847e0..c3b4aaed8e 100644
--- a/lang/de/devices.php
+++ b/lang/de/devices.php
@@ -62,7 +62,6 @@
'status' => 'Status',
'spare_parts' => 'Ersatzteile',
'latest_data' => 'Neueste Daten',
- 'table_intro' => 'Klicken Sie auf die „i"-Symbole für Details. Klicken Sie auf einen Spaltenkopf, um nach dieser Spalte zu sortieren – erneut klicken, um die Sortierreihenfolge umzukehren.',
'assessment' => 'Beurteilung',
'item_and_repair_info' => 'Artikel- und Reparaturinfo',
'search_assessment_comments' => 'Beurteilung',
diff --git a/lang/de/groups.php b/lang/de/groups.php
index 4fb009110e..618539a0f4 100644
--- a/lang/de/groups.php
+++ b/lang/de/groups.php
@@ -74,13 +74,11 @@
'invite_to_group' => 'Zur Gruppe einladen',
'participants' => 'Teilnehmende',
'hours_volunteered' => 'geleistete Freiwilligenstunden',
- 'years_volunteered' => 'geleistete Freiwilligenjahre',
'waste_prevented' => 'Vermiedener Abfall',
'upcoming_active' => 'Bevorstehend & Aktiv',
'past' => 'Vergangen',
'no_upcoming_events' => 'Derzeit gibt es keine bevorstehenden Veranstaltungen.',
'no_past_events' => 'Für diese Gruppe gibt es derzeit keine vergangenen Veranstaltungen',
- 'device_breakdown' => 'Geräteaufschlüsselung',
'total_devices' => 'Insgesamt bearbeitete Artikel',
'most_repaired_devices' => 'Am häufigsten reparierte Geräte',
'website' => 'Website',
@@ -103,7 +101,6 @@
'fixed_items' => 'Reparierte Artikel',
'repairable_items' => 'Reparierbare Artikel',
'end_of_life_items' => 'Artikel am Ende ihres Lebens',
- 'no_unpowered_stats' => 'Momentan werden diese Statistiken nur für elektrische Geräte angezeigt. Wir hoffen, bald auch nicht-elektrische Gegenstände einzubeziehen.',
'all_groups_mobile' => 'Alle',
'create_groups_mobile2' => 'Neu hinzufügen',
'groups_title1_mobile' => 'Meine',
diff --git a/lang/en/devices.php b/lang/en/devices.php
index 647f9da1c6..d35e119f4f 100644
--- a/lang/en/devices.php
+++ b/lang/en/devices.php
@@ -62,7 +62,6 @@
'status' => 'Status',
'spare_parts' => 'Spare parts',
'latest_data' => 'Latest Data',
- 'table_intro' => 'Press the ‘i’ icons for details. Click a column head to sort by that column - click again to reverse sort order.',
'assessment' => 'Assessment',
'item_and_repair_info' => 'Item & Repair Info',
'search_assessment_comments' => 'Assessment',
@@ -78,4 +77,15 @@
'image_delete_success' => 'Thank you, the image has been deleted',
'image_delete_error' => 'Sorry, but the image can\'t be deleted',
'image_upload_error' => 'fail - image could not be uploaded',
+ 'powered_items_fixed' => 'powered items fixed',
+ 'unpowered_items_fixed' => 'unpowered items fixed',
+ 'events_held' => 'events held',
+ 'items_seen' => 'items seen',
+ 'powered_or_unpowered' => 'Powered or unpowered',
+ 'both' => 'Both',
+ 'view_event_for_repair' => 'View the event for this repair',
+ 'powered' => 'Powered',
+ 'unpowered' => 'Unpowered',
+ 'view_record' => 'See the details of this repair',
+ 'edit_record' => 'See and edit the details of this repair',
];
diff --git a/lang/en/groups.php b/lang/en/groups.php
index 7a11f8d64f..7b6b17c185 100644
--- a/lang/en/groups.php
+++ b/lang/en/groups.php
@@ -73,15 +73,13 @@
'participants' => 'participants',
'parties_thrown' => 'parties thrown',
'hours_volunteered' => 'hours volunteered',
- 'years_volunteered' => 'years volunteered',
'waste_prevented' => 'Waste prevented',
'upcoming_active' => 'Upcoming & Active',
'past' => 'Past',
'no_upcoming_events' => 'There are currently no upcoming events.',
'no_past_events' => 'There are currently no past events for this group',
- 'device_breakdown' => 'Device breakdown',
'total_devices' => 'Total items worked on',
- 'most_repaired_devices' => 'Most repaired devices',
+ 'most_repaired_devices' => 'Most repaired items',
'website' => 'Website',
'about_none' => 'There is no description for this group yet.',
'read_less' => ' READ LESS',
@@ -102,7 +100,6 @@
'fixed_items' => 'Fixed items',
'repairable_items' => 'Repairable items',
'end_of_life_items' => 'End-of-life items',
- 'no_unpowered_stats' => 'At the moment, these stats are only displayed for powered items. We hope to include unpowered items soon.',
'create_groups_mobile2' => 'Add new',
'groups_title1_mobile' => 'Yours',
'groups_title2_mobile' => 'Find',
@@ -181,4 +178,7 @@
'search_nothing_found' => 'Nothing found.',
'goto_group' => 'Go to group',
'next_event' => 'Next event',
+ 'years_of_time_volunteered' => 'years of time volunteered',
+ 'see_group_reports' => 'See more in your group\'s reports',
+ 'view_reports' => 'View reports',
];
diff --git a/lang/en/networks.php b/lang/en/networks.php
index 61505b2603..94bb23484a 100644
--- a/lang/en/networks.php
+++ b/lang/en/networks.php
@@ -4,6 +4,8 @@
'networks' => 'Networks',
'network' => 'Network',
'general' => [
+ 'view_reports' => 'View reports',
+ 'see_network_reports' => 'See more in your network\'s reports',
'networks' => 'Networks',
'network' => 'Network',
'particular_network' => ':networkName network',
@@ -55,6 +57,8 @@
'none' => 'None',
],
'edit' => [
+ 'label_reporting_url' => 'Reporting dashboard address',
+ 'help_reporting_url' => 'Where this network\'s reports are published. Leave blank if the network has no reports.',
'label_logo' => 'Network logo',
'button_save' => 'Save changes',
'add_new_field' => 'Add new field',
diff --git a/lang/en/partials.php b/lang/en/partials.php
index 95cec7a55c..83fef3cf03 100644
--- a/lang/en/partials.php
+++ b/lang/en/partials.php
@@ -96,5 +96,6 @@
'share_modal_seedlings' => "tree seedling for 10 years.*|tree seedlings for 10 years.*",
'share_modal_planting_around' => "planting around",
'share_modal_hectares' => "hectare of trees.*|hectares of trees.*",
- 'impact_estimates' => 'Impact figures are estimates based on data entered by groups into Restarters.net.'
+ 'impact_estimates' => 'Impact figures are estimates based on data entered by groups into Restarters.net.',
+ 'see_further_reports' => 'See more analysis in our further reports.'
];
diff --git a/lang/es/devices.php b/lang/es/devices.php
index f96332604a..86b7b18f99 100644
--- a/lang/es/devices.php
+++ b/lang/es/devices.php
@@ -62,7 +62,6 @@
'status' => 'Estado',
'spare_parts' => 'Piezas de repuesto',
'latest_data' => 'Últimos datos',
- 'table_intro' => 'Pulsa los iconos «i» para ver los detalles. Haz clic en el encabezado de una columna para ordenar por esa columna; vuelve a hacer clic para invertir el orden.',
'assessment' => 'Evaluación',
'item_and_repair_info' => 'Info del artículo y la reparación',
'search_assessment_comments' => 'Evaluación',
diff --git a/lang/es/groups.php b/lang/es/groups.php
index fff0be774e..fa3e30721d 100644
--- a/lang/es/groups.php
+++ b/lang/es/groups.php
@@ -74,13 +74,11 @@
'invite_to_group' => 'Invitar al grupo',
'participants' => 'participantes',
'hours_volunteered' => 'horas de voluntariado',
- 'years_volunteered' => 'años de voluntariado',
'waste_prevented' => 'Residuos evitados',
'upcoming_active' => 'Próximos y activos',
'past' => 'Pasados',
'no_upcoming_events' => 'Actualmente no hay próximos eventos.',
'no_past_events' => 'Actualmente no hay eventos pasados para este grupo',
- 'device_breakdown' => 'Desglose de dispositivos',
'total_devices' => 'Total de artículos trabajados',
'most_repaired_devices' => 'Dispositivos más reparados',
'website' => 'Sitio web',
@@ -103,7 +101,6 @@
'fixed_items' => 'Artículos reparados',
'repairable_items' => 'Artículos reparables',
'end_of_life_items' => 'Artículos al final de su vida útil',
- 'no_unpowered_stats' => 'En este momento, estas estadísticas solo se muestran para artículos con alimentación eléctrica. Esperamos incluir artículos sin alimentación próximamente.',
'all_groups_mobile' => 'Todos',
'create_groups_mobile2' => 'Añadir nuevo',
'groups_title1_mobile' => 'Los tuyos',
diff --git a/lang/fr-BE/devices.php b/lang/fr-BE/devices.php
index e64556c516..7fe2b458ff 100644
--- a/lang/fr-BE/devices.php
+++ b/lang/fr-BE/devices.php
@@ -65,7 +65,6 @@
'assessment' => 'Evaluation',
'item_and_repair_info' => 'Appareils et informations de réparation',
'search_assessment_comments' => 'Evaluation',
- 'table_intro' => 'Cliquez sur la ligne pour plus de détails. Cliquez sur l\'en-tête d\'une colonne pour trier par cette colonne - cliquez à nouveau pour inverser l\'ordre de tri.',
'tooltip_type' => 'Ajoutez autant d\'informations que possible sur le type d\'objet (ex: "Jeans Denim", ou "divan")',
'add_data_event' => 'Veuillez sélectionner un événement',
'add_data_group' => 'Veuillez sélectionner un Repair Café',
@@ -78,4 +77,15 @@
'image_delete_success' => 'Merci, l\'image a été supprimée',
'image_delete_error' => 'Désolé, mais l\'image ne peut pas être supprimée.',
'image_upload_error' => 'fail - l\'image n\'a pas pu être téléchargée',
+ 'powered_items_fixed' => 'appareils électriques réparés',
+ 'unpowered_items_fixed' => 'objets non électriques réparés',
+ 'events_held' => 'événements organisés',
+ 'items_seen' => 'objets vus',
+ 'powered_or_unpowered' => 'Électrique ou non électrique',
+ 'both' => 'Les deux',
+ 'view_event_for_repair' => 'Voir l\'événement de cette réparation',
+ 'powered' => 'Électrique',
+ 'unpowered' => 'Non électrique',
+ 'view_record' => 'Voir les détails de cette réparation',
+ 'edit_record' => 'Voir et modifier les détails de cette réparation',
];
diff --git a/lang/fr-BE/groups.php b/lang/fr-BE/groups.php
index cfdeae755a..0a31421a7d 100644
--- a/lang/fr-BE/groups.php
+++ b/lang/fr-BE/groups.php
@@ -65,12 +65,11 @@
'create_groups_mobile' => 'créer',
'about' => 'A propos',
'add_event' => 'Ajouter événement',
- 'device_breakdown' => 'Panne de l\'appareil',
'group_actions' => 'Gestion du Repair Café',
'host' => 'Organisateur',
'invite_to_group' => 'Inviter au Repair Café',
'invite_volunteers' => 'Inviter des bénévoles',
- 'most_repaired_devices' => 'Les appareils les plus réparés',
+ 'most_repaired_devices' => 'Les objets les plus réparés',
'no_past_events' => 'Il n\'y a actuellement aucun événement passé pour ce Repair Café',
'no_upcoming_events' => 'Il n\'y a actuellement aucun événément à venir pour ce Repair Café',
'past' => 'Passé',
@@ -80,7 +79,6 @@
'participants' => 'participants',
'parties_thrown' => 'événements organisés',
'hours_volunteered' => 'heures de bénévolat',
- 'years_volunteered' => 'années de bénévolat',
'waste_prevented' => 'Déchets évités',
'upcoming_active' => 'A venir et actif',
'volunteers' => 'Bénévoles',
@@ -118,7 +116,6 @@
'no_groups_mine' => 'Si vous ne pouvez encore en voir aucun ici, pourquoi ne pas trouver un Repair Café près de chez vous?',
'no_groups_nearest_no_location' => '
Vous n\'avez pas défini de village/ville. Vous pouvez en ajouter un.e dans votre profil.
Vous pouvez aussi voir tous les Repair Cafés.
', 'no_groups_nearest_with_location' => 'Il n\'y a apparemment pas encore de Repair Cafés listé proche de chez vous.
Voulez-vous créer ou ajouter un Repair Café? Regardez comment faire dans nos ressources.
', - 'no_unpowered_stats' => 'Pour l\'instant, ces statistiques sont seulement affichées pour les appareils électriques. Nous espérons pouvoir inclure les appareils non-électriques sous peu.', 'search_country_placeholder' => 'Filtrer par pays', 'search_location_placeholder' => 'Rechercher par localisation', 'search_name_placeholder' => 'Rechercher par nom', @@ -184,4 +181,7 @@ 'search_nothing_found' => 'Aucun résultat.', 'goto_group' => 'Aller au Repair Café', 'next_event' => 'Prochain événement', + 'years_of_time_volunteered' => 'années de temps de bénévolat', + 'see_group_reports' => 'En savoir plus dans les rapports de votre groupe', + 'view_reports' => 'Voir les rapports', ]; diff --git a/lang/fr-BE/networks.php b/lang/fr-BE/networks.php index 84bf262c9b..87e7db3ad8 100644 --- a/lang/fr-BE/networks.php +++ b/lang/fr-BE/networks.php @@ -2,6 +2,8 @@ return [ 'edit' => [ + 'label_reporting_url' => 'Adresse du tableau de bord des rapports', + 'help_reporting_url' => 'L\'adresse où sont publiés les rapports de ce réseau. Laissez vide si le réseau n\'a pas de rapports.', 'button_save' => 'Sauver les changements', 'label_logo' => 'Logo du réseau', 'add_new_field' => 'Ajouter un nouveau champ', @@ -9,6 +11,8 @@ 'add_field' => 'Ajouter le champ', ], 'general' => [ + 'view_reports' => 'Voir les rapports', + 'see_network_reports' => 'En savoir plus dans les rapports de votre réseau', 'network' => 'Réseau', 'networks' => 'Réseaux', 'particular_network' => 'Réseau :networkName', diff --git a/lang/fr-BE/partials.php b/lang/fr-BE/partials.php index ab28284b87..b9df4ef8fa 100644 --- a/lang/fr-BE/partials.php +++ b/lang/fr-BE/partials.php @@ -100,4 +100,5 @@ 'share_modal_planting_around' => "planter environ", 'share_modal_hectares' => "hectare d’arbres.*|hectares d’arbres.*", 'impact_estimates' => 'Les chiffres relatifs à l\'impact sont des estimations basées sur les données saisies par les groupes dans Restarters.net.', + 'see_further_reports' => 'Voir plus d\'analyses dans nos rapports détaillés.', ]; diff --git a/lang/fr/devices.php b/lang/fr/devices.php index e64556c516..7fe2b458ff 100644 --- a/lang/fr/devices.php +++ b/lang/fr/devices.php @@ -65,7 +65,6 @@ 'assessment' => 'Evaluation', 'item_and_repair_info' => 'Appareils et informations de réparation', 'search_assessment_comments' => 'Evaluation', - 'table_intro' => 'Cliquez sur la ligne pour plus de détails. Cliquez sur l\'en-tête d\'une colonne pour trier par cette colonne - cliquez à nouveau pour inverser l\'ordre de tri.', 'tooltip_type' => 'Ajoutez autant d\'informations que possible sur le type d\'objet (ex: "Jeans Denim", ou "divan")', 'add_data_event' => 'Veuillez sélectionner un événement', 'add_data_group' => 'Veuillez sélectionner un Repair Café', @@ -78,4 +77,15 @@ 'image_delete_success' => 'Merci, l\'image a été supprimée', 'image_delete_error' => 'Désolé, mais l\'image ne peut pas être supprimée.', 'image_upload_error' => 'fail - l\'image n\'a pas pu être téléchargée', + 'powered_items_fixed' => 'appareils électriques réparés', + 'unpowered_items_fixed' => 'objets non électriques réparés', + 'events_held' => 'événements organisés', + 'items_seen' => 'objets vus', + 'powered_or_unpowered' => 'Électrique ou non électrique', + 'both' => 'Les deux', + 'view_event_for_repair' => 'Voir l\'événement de cette réparation', + 'powered' => 'Électrique', + 'unpowered' => 'Non électrique', + 'view_record' => 'Voir les détails de cette réparation', + 'edit_record' => 'Voir et modifier les détails de cette réparation', ]; diff --git a/lang/fr/groups.php b/lang/fr/groups.php index 24beb108dc..9d064a3bc9 100644 --- a/lang/fr/groups.php +++ b/lang/fr/groups.php @@ -65,12 +65,11 @@ 'create_groups_mobile' => 'créer', 'about' => 'A propos', 'add_event' => 'Ajouter événement', - 'device_breakdown' => 'Panne de l\'appareil', 'group_actions' => 'Gestion du Repair Café', 'host' => 'Organisateur', 'invite_to_group' => 'Inviter au Repair Café', 'invite_volunteers' => 'Inviter des bénévoles', - 'most_repaired_devices' => 'Les appareils les plus réparés', + 'most_repaired_devices' => 'Les objets les plus réparés', 'no_past_events' => 'Il n\'y a actuellement aucun événement passé pour ce Repair Café', 'no_upcoming_events' => 'Il n\'y a actuellement aucun événément à venir pour ce Repair Café', 'past' => 'Passé', @@ -80,7 +79,6 @@ 'participants' => 'participants', 'parties_thrown' => 'événements organisés', 'hours_volunteered' => 'heures de bénévolat', - 'years_volunteered' => 'années de bénévolat', 'waste_prevented' => 'Déchets évités', 'upcoming_active' => 'A venir et actif', 'volunteers' => 'Bénévoles', @@ -118,7 +116,6 @@ 'no_groups_mine' => 'Si vous ne pouvez encore en voir aucun ici, pourquoi ne pas trouver un Repair Café près de chez vous?', 'no_groups_nearest_no_location' => 'Vous n\'avez pas défini de village/ville. Vous pouvez en ajouter un.e dans votre profil.
Vous pouvez aussi voir tous les Repair Cafés.
', 'no_groups_nearest_with_location' => 'Il n\'y a apparemment pas encore de Repair Cafés listé proche de chez vous.
Voulez-vous créer ou ajouter un Repair Café? Regardez comment faire dans nos ressources.
', - 'no_unpowered_stats' => 'Pour l\'instant, ces statistiques sont seulement affichées pour les appareils électriques. Nous espérons pouvoir inclure les appareils non-électriques sous peu.', 'search_country_placeholder' => 'Filtrer par pays', 'search_location_placeholder' => 'Rechercher par localisation', 'search_name_placeholder' => 'Rechercher par nom', @@ -184,4 +181,7 @@ 'search_nothing_found' => 'Aucun résultat.', 'goto_group' => 'Aller au Repair Café', 'next_event' => 'Prochain événement', + 'years_of_time_volunteered' => 'années de temps de bénévolat', + 'see_group_reports' => 'En savoir plus dans les rapports de votre groupe', + 'view_reports' => 'Voir les rapports', ]; diff --git a/lang/fr/networks.php b/lang/fr/networks.php index 84bf262c9b..87e7db3ad8 100644 --- a/lang/fr/networks.php +++ b/lang/fr/networks.php @@ -2,6 +2,8 @@ return [ 'edit' => [ + 'label_reporting_url' => 'Adresse du tableau de bord des rapports', + 'help_reporting_url' => 'L\'adresse où sont publiés les rapports de ce réseau. Laissez vide si le réseau n\'a pas de rapports.', 'button_save' => 'Sauver les changements', 'label_logo' => 'Logo du réseau', 'add_new_field' => 'Ajouter un nouveau champ', @@ -9,6 +11,8 @@ 'add_field' => 'Ajouter le champ', ], 'general' => [ + 'view_reports' => 'Voir les rapports', + 'see_network_reports' => 'En savoir plus dans les rapports de votre réseau', 'network' => 'Réseau', 'networks' => 'Réseaux', 'particular_network' => 'Réseau :networkName', diff --git a/lang/fr/partials.php b/lang/fr/partials.php index ab28284b87..b9df4ef8fa 100644 --- a/lang/fr/partials.php +++ b/lang/fr/partials.php @@ -100,4 +100,5 @@ 'share_modal_planting_around' => "planter environ", 'share_modal_hectares' => "hectare d’arbres.*|hectares d’arbres.*", 'impact_estimates' => 'Les chiffres relatifs à l\'impact sont des estimations basées sur les données saisies par les groupes dans Restarters.net.', + 'see_further_reports' => 'Voir plus d\'analyses dans nos rapports détaillés.', ]; diff --git a/lang/it/devices.php b/lang/it/devices.php index 62cbf71331..d8c8990ee6 100644 --- a/lang/it/devices.php +++ b/lang/it/devices.php @@ -62,7 +62,6 @@ 'status' => 'Stato', 'spare_parts' => 'Parti di ricambio', 'latest_data' => 'Dati più recenti', - 'table_intro' => 'Premi le icone \'i\' per i dettagli. Clicca l\'intestazione di una colonna per ordinare per quella colonna - clicca di nuovo per invertire l\'ordine.', 'assessment' => 'Valutazione', 'item_and_repair_info' => 'Informazioni oggetto e riparazione', 'search_assessment_comments' => 'Valutazione', diff --git a/lang/it/groups.php b/lang/it/groups.php index 59e0d3411c..6cac8af26a 100644 --- a/lang/it/groups.php +++ b/lang/it/groups.php @@ -74,13 +74,11 @@ 'invite_to_group' => 'Invita nel gruppo', 'participants' => 'partecipanti', 'hours_volunteered' => 'ore di volontariato', - 'years_volunteered' => 'anni di volontariato', 'waste_prevented' => 'Rifiuti prevenuti', 'upcoming_active' => 'Prossimi e attivi', 'past' => 'Passati', 'no_upcoming_events' => 'Al momento non ci sono prossimi eventi.', 'no_past_events' => 'Al momento non ci sono eventi passati per questo gruppo', - 'device_breakdown' => 'Ripartizione dispositivi', 'total_devices' => 'Totale oggetti trattati', 'most_repaired_devices' => 'Dispositivi più riparati', 'website' => 'Website', @@ -103,7 +101,6 @@ 'fixed_items' => 'Oggetti riparati', 'repairable_items' => 'Oggetti riparabili', 'end_of_life_items' => 'Oggetti a fine vita', - 'no_unpowered_stats' => 'Al momento queste statistiche sono visualizzate solo per gli oggetti alimentati. Speriamo di includere presto gli oggetti non alimentati.', 'all_groups_mobile' => 'Tutti', 'create_groups_mobile2' => 'Aggiungi nuovo', 'groups_title1_mobile' => 'Tuoi', diff --git a/lang/ne/devices.php b/lang/ne/devices.php index 50e2aa2e10..b91591253d 100644 --- a/lang/ne/devices.php +++ b/lang/ne/devices.php @@ -62,7 +62,6 @@ 'status' => 'Status', 'spare_parts' => 'Reserveonderdelen', 'latest_data' => 'Meest recente gegevens', - 'table_intro' => 'Klik op de \'i\'-pictogrammen voor details. Klik op een kolomkop om op die kolom te sorteren - klik nogmaals om de sorteervolgorde om te keren.', 'assessment' => 'Beoordeling', 'item_and_repair_info' => 'Item- en reparatie-informatie', 'search_assessment_comments' => 'Beoordeling', diff --git a/lang/ne/groups.php b/lang/ne/groups.php index 93fdae5cc0..683d881841 100644 --- a/lang/ne/groups.php +++ b/lang/ne/groups.php @@ -74,13 +74,11 @@ 'invite_to_group' => 'Uitnodigen voor groep', 'participants' => 'deelnemers', 'hours_volunteered' => 'vrijwilligersuren', - 'years_volunteered' => 'vrijwilligersjaren', 'waste_prevented' => 'Afval voorkomen', 'upcoming_active' => 'Aankomend & Actief', 'past' => 'Afgelopen', 'no_upcoming_events' => 'Er zijn momenteel geen aankomende evenementen.', 'no_past_events' => 'Er zijn momenteel geen afgelopen evenementen voor deze groep', - 'device_breakdown' => 'Apparatenanalyse', 'total_devices' => 'Totaal aantal items bewerkt', 'most_repaired_devices' => 'Meest gerepareerde apparaten', 'website' => 'Website', @@ -103,7 +101,6 @@ 'fixed_items' => 'Gerepareerde items', 'repairable_items' => 'Repareerbare items', 'end_of_life_items' => 'Items aan einde levensduur', - 'no_unpowered_stats' => 'Op dit moment worden deze statistieken alleen weergegeven voor elektrische items. We hopen binnenkort ook niet-elektrische items op te nemen.', 'all_groups_mobile' => 'Alle', 'create_groups_mobile2' => 'Nieuw toevoegen', 'groups_title1_mobile' => 'Van mij', diff --git a/lang/nl-BE/devices.php b/lang/nl-BE/devices.php index 0b9038d21d..b4052cd216 100644 --- a/lang/nl-BE/devices.php +++ b/lang/nl-BE/devices.php @@ -62,7 +62,6 @@ 'status' => 'Status', 'spare_parts' => 'Reserveonderdelen', 'latest_data' => 'Meest recente gegevens', - 'table_intro' => 'Druk op de \'i\'-pictogrammen voor details. Klik op een kolomhoofd om op die kolom te sorteren - klik opnieuw om de sorteervolgorde om te keren.', 'assessment' => 'Beoordeling', 'item_and_repair_info' => 'Item- en herstelinfo', 'search_assessment_comments' => 'Beoordeling', diff --git a/lang/nl-BE/groups.php b/lang/nl-BE/groups.php index 53a0671f4f..475ad6097e 100644 --- a/lang/nl-BE/groups.php +++ b/lang/nl-BE/groups.php @@ -74,13 +74,11 @@ 'invite_to_group' => 'Uitnodigen voor groep', 'participants' => 'deelnemers', 'hours_volunteered' => 'uren vrijwilligerswerk', - 'years_volunteered' => 'jaren vrijwilligerswerk', 'waste_prevented' => 'Afval vermeden', 'upcoming_active' => 'Komend & Actief', 'past' => 'Voorbij', 'no_upcoming_events' => 'Er zijn momenteel geen komende activiteiten.', 'no_past_events' => 'Er zijn momenteel geen voorbije activiteiten voor deze groep', - 'device_breakdown' => 'Overzicht van toestellen', 'total_devices' => 'Totaal aantal behandelde items', 'most_repaired_devices' => 'Meest herstelde toestellen', 'website' => 'Website', @@ -103,7 +101,6 @@ 'fixed_items' => 'Herstelde items', 'repairable_items' => 'Herstelbare items', 'end_of_life_items' => 'Onherstelbare items', - 'no_unpowered_stats' => 'Momenteel worden deze statistieken alleen weergegeven voor elektrische items. We hopen binnenkort ook niet-elektrische items op te nemen.', 'all_groups_mobile' => 'Alle', 'create_groups_mobile2' => 'Nieuw toevoegen', 'groups_title1_mobile' => 'Jouw', diff --git a/lang/nl/devices.php b/lang/nl/devices.php index 50e2aa2e10..b91591253d 100644 --- a/lang/nl/devices.php +++ b/lang/nl/devices.php @@ -62,7 +62,6 @@ 'status' => 'Status', 'spare_parts' => 'Reserveonderdelen', 'latest_data' => 'Meest recente gegevens', - 'table_intro' => 'Klik op de \'i\'-pictogrammen voor details. Klik op een kolomkop om op die kolom te sorteren - klik nogmaals om de sorteervolgorde om te keren.', 'assessment' => 'Beoordeling', 'item_and_repair_info' => 'Item- en reparatie-informatie', 'search_assessment_comments' => 'Beoordeling', diff --git a/lang/nl/groups.php b/lang/nl/groups.php index 93fdae5cc0..683d881841 100644 --- a/lang/nl/groups.php +++ b/lang/nl/groups.php @@ -74,13 +74,11 @@ 'invite_to_group' => 'Uitnodigen voor groep', 'participants' => 'deelnemers', 'hours_volunteered' => 'vrijwilligersuren', - 'years_volunteered' => 'vrijwilligersjaren', 'waste_prevented' => 'Afval voorkomen', 'upcoming_active' => 'Aankomend & Actief', 'past' => 'Afgelopen', 'no_upcoming_events' => 'Er zijn momenteel geen aankomende evenementen.', 'no_past_events' => 'Er zijn momenteel geen afgelopen evenementen voor deze groep', - 'device_breakdown' => 'Apparatenanalyse', 'total_devices' => 'Totaal aantal items bewerkt', 'most_repaired_devices' => 'Meest gerepareerde apparaten', 'website' => 'Website', @@ -103,7 +101,6 @@ 'fixed_items' => 'Gerepareerde items', 'repairable_items' => 'Repareerbare items', 'end_of_life_items' => 'Items aan einde levensduur', - 'no_unpowered_stats' => 'Op dit moment worden deze statistieken alleen weergegeven voor elektrische items. We hopen binnenkort ook niet-elektrische items op te nemen.', 'all_groups_mobile' => 'Alle', 'create_groups_mobile2' => 'Nieuw toevoegen', 'groups_title1_mobile' => 'Van mij', diff --git a/lang/no/devices.php b/lang/no/devices.php index be135143ce..d05631d935 100644 --- a/lang/no/devices.php +++ b/lang/no/devices.php @@ -62,7 +62,6 @@ 'status' => 'Status', 'spare_parts' => 'Reservedeler', 'latest_data' => 'Siste data', - 'table_intro' => 'Trykk på «i»-ikonene for detaljer. Klikk på en kolonneoverskrift for å sortere etter den kolonnen – klikk igjen for omvendt sorteringsrekkefølge.', 'assessment' => 'Vurdering', 'item_and_repair_info' => 'Gjenstand og reparasjonsinfo', 'search_assessment_comments' => 'Vurdering', diff --git a/lang/no/groups.php b/lang/no/groups.php index e52f37225e..0d0578c229 100644 --- a/lang/no/groups.php +++ b/lang/no/groups.php @@ -74,13 +74,11 @@ 'invite_to_group' => 'Inviter til gruppe', 'participants' => 'deltakere', 'hours_volunteered' => 'timer frivillig arbeid', - 'years_volunteered' => 'år frivillig arbeid', 'waste_prevented' => 'Forhindret avfall', 'upcoming_active' => 'Kommende og aktive', 'past' => 'Tidligere', 'no_upcoming_events' => 'Det er for øyeblikket ingen kommende arrangementer.', 'no_past_events' => 'Det er for øyeblikket ingen tidligere arrangementer for denne gruppen', - 'device_breakdown' => 'Enhetsfordeling', 'total_devices' => 'Totalt antall gjenstander arbeidet med', 'most_repaired_devices' => 'Mest reparerte enheter', 'website' => 'Nettsted', @@ -103,7 +101,6 @@ 'fixed_items' => 'Reparerte gjenstander', 'repairable_items' => 'Reparerbare gjenstander', 'end_of_life_items' => 'Gjenstander ved slutten av levetid', - 'no_unpowered_stats' => 'For øyeblikket vises disse statistikkene kun for elektriske gjenstander. Vi håper å inkludere ikke-elektriske gjenstander snart.', 'all_groups_mobile' => 'Alle', 'create_groups_mobile2' => 'Legg til ny', 'groups_title1_mobile' => 'Dine', diff --git a/resources/js/components/DeviceCategorySelect.vue b/resources/js/components/DeviceCategorySelect.vue index 44064e5c73..99de39ef9a 100644 --- a/resources/js/components/DeviceCategorySelect.vue +++ b/resources/js/components/DeviceCategorySelect.vue @@ -21,7 +21,7 @@ @open="onOpen" > -{{ __('devices.search_text') }}
{{ __('devices.table_intro') }}
+- {{ __('groups.no_unpowered_stats') }} -
-