From f27ffc1ce8a2646d633683c17949994931abfa9f Mon Sep 17 00:00:00 2001 From: Romulus21 Date: Sat, 17 Feb 2024 00:02:39 +0100 Subject: [PATCH] add duration --- .../Controllers/TimeTrackerController.php | 10 +++- app/Http/Resources/TimeTrackerResource.php | 2 +- app/Mail/Reset.php | 1 - app/Models/User.php | 7 ++- database/factories/UserFactory.php | 2 +- resources/js/components/Header.tsx | 28 +++++---- resources/js/components/Modals.tsx | 44 ++++++++++++++ resources/js/components/SVG.tsx | 4 ++ .../TimeTrackers/TimeTrackerEdit.tsx | 38 ++++++++++++ .../components/{ => TimeTrackers}/Tracker.tsx | 13 +++-- resources/js/components/toDos/ToDoFinish.tsx | 9 ++- resources/js/components/toDos/ToDoIndex.tsx | 26 ++++++--- resources/js/components/toDos/ToDoShow.tsx | 9 +-- resources/js/hooks/TraskerHook.tsx | 8 +-- resources/js/pages/Router.tsx | 2 + resources/js/pages/TimeTrackersIndex.tsx | 58 +++++++++++++++++++ resources/js/utilities/customProperties.ts | 25 +++++++- routes/api.php | 6 +- tests/Feature/TimeTrackerTest.php | 4 +- 19 files changed, 248 insertions(+), 48 deletions(-) create mode 100644 resources/js/components/Modals.tsx create mode 100644 resources/js/components/TimeTrackers/TimeTrackerEdit.tsx rename resources/js/components/{ => TimeTrackers}/Tracker.tsx (75%) create mode 100644 resources/js/pages/TimeTrackersIndex.tsx diff --git a/app/Http/Controllers/TimeTrackerController.php b/app/Http/Controllers/TimeTrackerController.php index 2ba55b6..abcd250 100644 --- a/app/Http/Controllers/TimeTrackerController.php +++ b/app/Http/Controllers/TimeTrackerController.php @@ -12,9 +12,15 @@ class TimeTrackerController extends Controller /** * Display a listing of the resource. */ - public function index() + public function index(Request $request) { - // + $timeTrackers = $request->user() + ->timeTrackers() + ->with('toDo') + ->orderBy('start_at', 'desc') + ->paginate(); + + return response()->json(TimeTrackerResource::collection($timeTrackers)); } /** diff --git a/app/Http/Resources/TimeTrackerResource.php b/app/Http/Resources/TimeTrackerResource.php index 5abfa15..90b9172 100644 --- a/app/Http/Resources/TimeTrackerResource.php +++ b/app/Http/Resources/TimeTrackerResource.php @@ -18,7 +18,7 @@ class TimeTrackerResource extends JsonResource 'id' => $this->id, 'start_at' => $this->start_at->format('Y-m-d H:i:s'), 'end_at' => $this->end_at?->format('Y-m-d H:i:s'), - 'to_do' => new ToDoResource($this->whenLoaded('toDo')) + 'to_do' => new ToDoResource($this->whenLoaded('toDo')), ]; } } diff --git a/app/Mail/Reset.php b/app/Mail/Reset.php index d951b3e..524f602 100644 --- a/app/Mail/Reset.php +++ b/app/Mail/Reset.php @@ -3,7 +3,6 @@ namespace App\Mail; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; diff --git a/app/Models/User.php b/app/Models/User.php index 0aeaa77..9cf7fb5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,7 +6,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -51,6 +51,11 @@ class User extends Authenticatable return $this->belongsTo(TimeTracker::class, 'time_tracker_id'); } + public function timeTrackers(): HasManyThrough + { + return $this->hasManyThrough(TimeTracker::class, ToDo::class); + } + public function toDos(): HasMany { return $this->hasMany(ToDo::class); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 5ec8c98..8264788 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -28,7 +28,7 @@ class UserFactory extends Factory 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), -// 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + // 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; } diff --git a/resources/js/components/Header.tsx b/resources/js/components/Header.tsx index 0674ade..4f9aba1 100644 --- a/resources/js/components/Header.tsx +++ b/resources/js/components/Header.tsx @@ -1,7 +1,7 @@ import React from "react" -import {Link, useLocation} from "react-router-dom" +import {Link, NavLink, useLocation} from "react-router-dom" import useAuthUser from "../hooks/AuthUser"; -import Tracker from "./Tracker"; +import Tracker from "./TimeTrackers/Tracker"; const Header = () => { @@ -10,20 +10,26 @@ const Header = () => { console.log(authUser) - return
-
- Ticcat -
+ return
+
+
+ Ticcat +
- {authUser && } - {authUser - ? + {authUser && } + {authUser + ? {authUser.name} - : + : Connexion - {/*S'inscrire*/} + {/*S'inscrire*/} } +
+
} diff --git a/resources/js/components/Modals.tsx b/resources/js/components/Modals.tsx new file mode 100644 index 0000000..834ef9f --- /dev/null +++ b/resources/js/components/Modals.tsx @@ -0,0 +1,44 @@ +import React, {FC, FormEvent, PropsWithChildren, ReactNode} from "react" +import ReactDOM from "react-dom" + +export const Modal: FC = ({children, show, closeModal, ...props}) => { + + const stopEvent = (event: FormEvent) => { + event.stopPropagation() + event.preventDefault() + } + + return show && +
+ {children} +
+
+} + +interface ModalProps { + children: ReactNode, + show: boolean, + closeModal: () => void +} + +const Overlay: FC = ({children, ...props}) => { + + const app = document.getElementById('app') + + if (app) { + return ReactDOM.createPortal( + , + app + ) + } +} + +interface OverlayProps { + children: ReactNode, + onClick: () => void +} diff --git a/resources/js/components/SVG.tsx b/resources/js/components/SVG.tsx index 90452f7..48b2aae 100644 --- a/resources/js/components/SVG.tsx +++ b/resources/js/components/SVG.tsx @@ -23,6 +23,10 @@ export const DraggableSVG: FC> = (props) => SVGSkeleton({ ...props }) +export const PauseSVG: FC> = (props) => SVGSkeleton({ + paths: , + ...props +}) export const PlaySVG: FC> = (props) => SVGSkeleton({ viewBox: "0 0 448 512", paths: = ({timeTracker}) => { + + const [trackerForm, setTrackerForm] = useState(timeTracker) + + const handleChange = (event: React.ChangeEvent) => { + console.log(trackerForm, event.target.value) + setTrackerForm({...trackerForm, [event.target.name]: event.target.value.replace('T', ' ')}) + } + + const onSubmit = (event: FormEvent) => { + event.preventDefault() + + console.log(trackerForm) + } + + return
+ + + + + +} + +export default TimeTrackerEdit + +interface TimeTrackerEditProps { + timeTracker: timeTracker, +} diff --git a/resources/js/components/Tracker.tsx b/resources/js/components/TimeTrackers/Tracker.tsx similarity index 75% rename from resources/js/components/Tracker.tsx rename to resources/js/components/TimeTrackers/Tracker.tsx index 6466ff8..ad1002b 100644 --- a/resources/js/components/Tracker.tsx +++ b/resources/js/components/TimeTrackers/Tracker.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from "react" -import useTracker from "../hooks/TraskerHook" +import useTracker from "../../hooks/TraskerHook" import {Link} from "react-router-dom"; -import {StopSVG} from "./SVG"; +import {StopSVG} from "../SVG"; const Tracker = () => { @@ -21,10 +21,11 @@ const Tracker = () => { return '--:--' } let timer = Math.floor(((new Date()).getTime() - (new Date(startAt)).getTime()) / 1000) - let hours = Math.floor(timer / 3600) - let minutes = Math.floor((timer - hours * 3600) / 60) - let secondes = timer - hours * 3600 - minutes * 60 - return `${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}` + return timer.durationify() + // let hours = Math.floor(timer / 3600) + // let minutes = Math.floor((timer - hours * 3600) / 60) + // let secondes = timer - hours * 3600 - minutes * 60 + // return `${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}` } return
diff --git a/resources/js/components/toDos/ToDoFinish.tsx b/resources/js/components/toDos/ToDoFinish.tsx index 383af5e..1b761cb 100644 --- a/resources/js/components/toDos/ToDoFinish.tsx +++ b/resources/js/components/toDos/ToDoFinish.tsx @@ -43,8 +43,13 @@ const ToDoFinish: FC = ({reload}) => { {errorLabel()} - {showTodos &&
    - {toDos.map(toDo =>
  • {toDo.checked ? (new Date(toDo.checked)).toSmallFrDate() : ''} {toDo.name}
  • )} + {showTodos &&
      + {toDos.map(toDo =>
    • + {toDo.checked ? (new Date(toDo.checked)).toSmallFrDate() : ''} + {toDo.name} + {toDo.duration.durationify()} + +
    • )}
    }
} diff --git a/resources/js/components/toDos/ToDoIndex.tsx b/resources/js/components/toDos/ToDoIndex.tsx index fde8630..f123cfd 100644 --- a/resources/js/components/toDos/ToDoIndex.tsx +++ b/resources/js/components/toDos/ToDoIndex.tsx @@ -5,13 +5,13 @@ import {Link} from "react-router-dom"; import useTracker from "../../hooks/TraskerHook"; import {Simulate} from "react-dom/test-utils"; import load = Simulate.load; -import {DraggableSVG, PlaySVG} from "../SVG"; +import {DraggableSVG, PauseSVG, PlaySVG} from "../SVG"; const ToDoIndex: FC = ({reload, setReload}) => { const {loading, setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true) const [toDos, setToDos] = useState([]) - const {startTrackToDo} = useTracker() + const {currentTimeTracker, startTrackToDo, stopCurrentTimeTrack} = useTracker() useEffect(() => { fetchToDos() @@ -59,13 +59,21 @@ const ToDoIndex: FC = ({reload, setReload}) => { {toDo.name} - {toDo.duration} s + {toDo.duration.durationify()} - {!toDo.checked && startTrackToDo(toDo)}> - - } + {toDo.id === currentTimeTracker?.to_do?.id + ? + : } )} @@ -74,6 +82,6 @@ const ToDoIndex: FC = ({reload, setReload}) => { export default ToDoIndex interface ToDoIndexProps { - reload: Date|null, + reload: Date | null, setReload: (date: Date) => void, } diff --git a/resources/js/components/toDos/ToDoShow.tsx b/resources/js/components/toDos/ToDoShow.tsx index 8b22ba4..1a2cefd 100644 --- a/resources/js/components/toDos/ToDoShow.tsx +++ b/resources/js/components/toDos/ToDoShow.tsx @@ -68,10 +68,11 @@ const ToDoTimeTrackers: FC = ({toDo: toDo}) => { }) timer = Math.floor(timer / 1000) - let hours = Math.floor(timer / 3600) - let minutes = Math.floor((timer - hours * 3600) / 60) - let secondes = timer - hours * 3600 - minutes * 60 - return `${more ? '+' : ''} ${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}` + return (more ? '+' : '') + timer.durationify() + // let hours = Math.floor(timer / 3600) + // let minutes = Math.floor((timer - hours * 3600) / 60) + // let secondes = timer - hours * 3600 - minutes * 60 + // return `${more ? '+' : ''} ${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}` } return
diff --git a/resources/js/hooks/TraskerHook.tsx b/resources/js/hooks/TraskerHook.tsx index c02ebc0..597e8d4 100644 --- a/resources/js/hooks/TraskerHook.tsx +++ b/resources/js/hooks/TraskerHook.tsx @@ -13,7 +13,7 @@ interface TrackerProps { } export const TrackerProvider = ({children}: PropsWithChildren) => { - const [currentTimeTracker, setCurrentTimeTracker] = useState(null) + const [currentTimeTracker, setCurrentTimeTracker] = useState(null) const [toDoTracked, setToDoTracked] = useState(null) const {axiosGet, axiosPost, axiosDelete} = useAxiosTools() @@ -23,7 +23,7 @@ export const TrackerProvider = ({children}: PropsWithChildren) => { const fetchCurrentTimeTracker = async () => { try { - const res = await axiosGet(`/api/time-tracker/user`) + const res = await axiosGet(`/api/time-trackers/user`) setCurrentTimeTracker(res.data) } catch (error) { console.error(error) @@ -32,7 +32,7 @@ export const TrackerProvider = ({children}: PropsWithChildren) => { const startTrackToDo = async (toDo: toDo) => { try { - const res = await axiosPost('/api/time-tracker', {todo_id: toDo.id}) + const res = await axiosPost('/api/time-trackers', {todo_id: toDo.id}) setCurrentTimeTracker(res.data) } catch (error) { console.error(error) @@ -41,7 +41,7 @@ export const TrackerProvider = ({children}: PropsWithChildren) => { const stopCurrentTimeTrack = async () => { try { - const res = await axiosDelete(`/api/time-tracker/user`) + const res = await axiosDelete(`/api/time-trackers/user`) setCurrentTimeTracker(null) } catch (error) { console.error(error) diff --git a/resources/js/pages/Router.tsx b/resources/js/pages/Router.tsx index 1223cf7..c337990 100644 --- a/resources/js/pages/Router.tsx +++ b/resources/js/pages/Router.tsx @@ -8,6 +8,7 @@ import Home from "./Home"; import useAuthUser from "../hooks/AuthUser"; import Register from "./Auth/Register"; import ToDoShow from "../components/toDos/ToDoShow"; +import TimeTrackersIndex from "./TimeTrackersIndex"; const Router = () => { @@ -25,6 +26,7 @@ const Router = () => { } /> } /> } /> + } /> diff --git a/resources/js/pages/TimeTrackersIndex.tsx b/resources/js/pages/TimeTrackersIndex.tsx new file mode 100644 index 0000000..0b31dd6 --- /dev/null +++ b/resources/js/pages/TimeTrackersIndex.tsx @@ -0,0 +1,58 @@ +import React, {useEffect, useState} from "react" +import useAxiosTools from "../hooks/AxiosTools"; +import {timeTracker, toDo} from "../utilities/types"; +import {PlaySVG} from "../components/SVG"; +import useTracker from "../hooks/TraskerHook"; +import {Modal} from "../components/Modals"; +import TimeTrackerEdit from "../components/TimeTrackers/TimeTrackerEdit"; + +const TimeTrackersIndex = () => { + + const {loading, setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true) + const [timeTrackers, setTimeTrackers] = useState([]) + const [showTrackers, setShowTrackers] = useState(null) + const {startTrackToDo} = useTracker() + + useEffect(() => { + fetchTimeTrackers() + }, []) + + const fetchTimeTrackers = async () => { + try { + const res = await axiosGet('api/time-trackers') + setTimeTrackers(res.data) + } catch (error) { + errorCatch(error) + } finally { + setLoading(false) + } + } + + return
+ {errorLabel()} +
    + {timeTrackers.map(tracker =>
  • + {tracker.start_at ? (new Date(tracker.start_at)).toSmallFrDate() : ''} + {(new Date(tracker.end_at)).toSmallFrDate()} + {tracker.to_do.name} + + {!tracker.to_do.checked && } + + +
  • )} +
+ + setShowTrackers(null)}> + {showTrackers && } + +
+} + +export default TimeTrackersIndex diff --git a/resources/js/utilities/customProperties.ts b/resources/js/utilities/customProperties.ts index eb2c2e1..44cf4ec 100644 --- a/resources/js/utilities/customProperties.ts +++ b/resources/js/utilities/customProperties.ts @@ -1,12 +1,31 @@ interface Number { - pad(n: number, char?: string): string + pad(n: number, char?: string): string, + durationify(): string } Number.prototype.pad = function(n, char = "0") { return (new Array(n).join(char) + this).slice(-n) } +Number.prototype.durationify = function () { + if (! this) { + return '0s' + } + const base = Number(this) + const days = Math.floor(base / 60 / 60 / 24) + const hours = Math.floor((base - (days * 60 * 60 * 24)) / 60 / 60) + const minutes = Math.floor((base - (days * 60 * 60 * 24) - (hours * 60 *60)) / 60) + const secondes = Math.floor(base - (days * 60 * 60 * 24) - (hours * 60 *60) - (minutes *60)) + + let duration = '' + duration += (days) ? `${days}j ` : '' + duration += (days > 0 || hours > 0) ? `${hours}h ` : '' + duration += (days > 0 || hours > 0 || minutes > 0) ? `${minutes}m ` : '' + duration += `${secondes}s` + return duration +} + interface Date { toFrDate(): string, toFrTime(): string, @@ -27,6 +46,10 @@ Date.prototype.toSmallFrDate = function() { return `${this.toFrTime()}` } + if ((new Date(this)).getFullYear() === (new Date()).getFullYear()) { + return `${this.getDate()}/${Number(this.getMonth() + 1).pad(2)} ${this.toFrTime()}` + } + return `${this.toFrDate()} ${this.toFrTime()}` } diff --git a/routes/api.php b/routes/api.php index d22d858..f0f057b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -25,13 +25,13 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/user', [AuthController::class, 'user'])->name('user'); Route::delete('/logout', [AuthController::class, 'logout'])->name('logout'); - Route::get('/time-tracker/user', [TimeTrackerController::class, 'userTimeTracker'])->name('time-tracker.user'); + Route::get('/time-trackers/user', [TimeTrackerController::class, 'userTimeTracker'])->name('time-tracker.user'); Route::get('/todos/{toDo}/time-trackers', [TimeTrackerController::class, 'toDoTimeTrackers'])->name('todos.time-trackers'); Route::get('/todos/finished', [ToDoController::class, 'finished'])->name('todos.finished'); - Route::delete('/time-tracker/user', [TimeTrackerController::class, 'stopUserTimeTracker'])->name('time-tracker.stop'); + Route::delete('/time-trackers/user', [TimeTrackerController::class, 'stopUserTimeTracker'])->name('time-tracker.stop'); Route::apiResources([ - 'time-tracker' => TimeTrackerController::class, + 'time-trackers' => TimeTrackerController::class, 'todos' => ToDoController::class, ]); }); diff --git a/tests/Feature/TimeTrackerTest.php b/tests/Feature/TimeTrackerTest.php index d38f0f0..fcd5ec1 100644 --- a/tests/Feature/TimeTrackerTest.php +++ b/tests/Feature/TimeTrackerTest.php @@ -19,7 +19,7 @@ test('user can start a time tracker', function () { 'user_id' => $user->id, 'name' => $toDo->name, 'checked' => false, - ] + ], ]); }); @@ -41,7 +41,7 @@ test('user can retrieve his current timer', function () { 'user_id' => $user->id, 'name' => $toDo->name, 'checked' => false, - ] + ], ]); });