-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-- SPDX-License-Identifier: AGPL-3.0-only
-- Copyright 2026 Shota FUJI
--
-- This program is free software: you can redistribute it and/or modify it under the terms
-- of the GNU Affero General Public License as published by the Free Software Foundation,
-- either version 3 of the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT ANY
-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
-- PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <http://www.gnu.org/licenses/>.
module Main exposing (main)
import Browser
import Browser.Navigation
import Html exposing (text)
import Html.Attributes exposing (href, style)
import Url
import Url.Parser exposing ((</>))
main : Program Flags Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = OnUrlChange
, onUrlRequest = OnUrlRequest
}
-- FLAGS
type alias Flags =
()
-- MODEL
type Editability
= Readonly
| ReadWrite
type User
= Worker
| Employer
| Admin
type Route
= DemoIndex
| WorkerHome Editability
| EmployerHome
| AdminHome
| WorkerSchedule User Editability
| WorkersList User
| NotFound
routeParser : Url.Parser.Parser (Route -> a) a
routeParser =
Url.Parser.oneOf
[ Url.Parser.map DemoIndex Url.Parser.top
, Url.Parser.map (WorkerHome Readonly) (Url.Parser.s "worker")
, Url.Parser.map (WorkerHome ReadWrite) (Url.Parser.s "worker-mutable")
, Url.Parser.map EmployerHome (Url.Parser.s "employer")
, Url.Parser.map AdminHome (Url.Parser.s "admin")
, Url.Parser.map (WorkerSchedule Worker Readonly) (Url.Parser.s "worker" </> Url.Parser.s "schedule")
, Url.Parser.map (WorkerSchedule Worker ReadWrite) (Url.Parser.s "worker" </> Url.Parser.s "schedule-mutable")
, Url.Parser.map (WorkerSchedule Employer ReadWrite) (Url.Parser.s "employer" </> Url.Parser.s "worker-schedule")
, Url.Parser.map (WorkersList Employer) (Url.Parser.s "employer" </> Url.Parser.s "workers")
]
type alias WorkerInfo =
{ displayName : String
, properties : List ( String, String )
}
type alias Model =
{ route : Route
, key : Browser.Navigation.Key
, workers : List WorkerInfo
}
init : Flags -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg )
init _ url key =
( { route = Url.Parser.parse routeParser url |> Maybe.withDefault NotFound
, key = key
, workers =
[ { displayName = "田中 太郎"
, properties =
[ ( "部署", "介護事業部" )
, ( "役職", "課長" )
]
}
, { displayName = "山田 花子"
, properties =
[ ( "部署", "介護事業部" )
, ( "役職", "パート" )
]
}
]
}
, Cmd.none
)
-- UPDATE
type Msg
= NoOp
| OnUrlChange Url.Url
| OnUrlRequest Browser.UrlRequest
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnUrlChange url ->
( { model
| route = Url.Parser.parse routeParser url |> Maybe.withDefault NotFound
}
, Cmd.none
)
OnUrlRequest (Browser.Internal url) ->
( model, Browser.Navigation.pushUrl model.key (Url.toString url) )
OnUrlRequest (Browser.External href) ->
( model, Browser.Navigation.load href )
NoOp ->
( model, Cmd.none )
-- VIEW
type PaidLeaveStatus
= Pending
| ManuallyApproved String
| AutoApproved
view : Model -> Browser.Document Msg
view model =
case model.route of
NotFound ->
{ title = "404 | Yamori デモ"
, body = [ Html.p [] [ text "存在しないページです" ] ]
}
WorkerHome editability ->
{ title = "労働者ホーム | Yamori デモ"
, body =
[ Html.h1 [] [ text "ようこそ、田中 太郎さん。" ]
, Html.p []
[ text "残り"
, Html.strong [ style "font-size" "1.5em" ] [ text "10" ]
, text "日の有給休暇が利用可能です。"
]
, Html.p []
[ text "労働基準法により、"
, Html.strong [] [ text "2026/02/01" ]
, text "までに"
, Html.strong [ style "font-size" "1.5em" ] [ text "3" ]
, text "日の有給休暇を"
, Html.mark [] [ text "利用する必要" ]
, text "があります。"
]
, Html.hr [] []
, Html.dl []
(case editability of
Readonly ->
[ Html.dt []
[ Html.a [ href "#" ] [ text "予定を確認する" ] ]
, Html.dd []
[ text "有給休暇の利用予定と実績を閲覧する画面に移動します。" ]
]
ReadWrite ->
[ Html.dt []
[ Html.a [ href "#" ] [ text "予定を確認する" ] ]
, Html.dd []
[ text "有給休暇の利用予定と実績の閲覧・編集画面に移動します。" ]
]
)
]
}
EmployerHome ->
{ title = "使用者ホーム | Yamori デモ"
, body =
[ Html.h1 [] [ text "株式会社XXX 有給休暇管理システム" ]
, Html.dl []
[ Html.dt []
[ Html.a [ href "employer/workers" ] [ text "労働者の一覧を見る" ] ]
, Html.dd []
[ text "システムに登録されている労働者の閲覧や変更を行う画面に移動します。" ]
, Html.dt []
[ Html.a [ href "#" ] [ text "全体の予定と実績を確認する" ] ]
, Html.dd []
[ text "すべての労働者の予定と実績をまとめて確認できる画面に移動します。" ]
]
]
}
AdminHome ->
{ title = "管理者ホーム | Yamori デモ"
, body =
[ Html.h1 [] [ text "株式会社XXX 有給休暇管理システム" ]
, Html.dl []
[ Html.dt []
[ Html.a [ href "#" ] [ text "労働者の一覧を見る" ] ]
, Html.dd []
[ text "システムに登録されている労働者の閲覧や変更を行う画面に移動します。" ]
, Html.dt []
[ Html.a [ href "#" ] [ text "全体の予定と実績を確認する" ] ]
, Html.dd []
[ text "すべての労働者の予定と実績をまとめて確認できる画面に移動します。" ]
, Html.dt []
[ Html.a [ href "#" ] [ text "カレンダーを読み込む" ] ]
, Html.dd []
[ text "スプレッドシート形式の勤怠データをシステムに反映します。" ]
]
]
}
WorkerSchedule role editability ->
{ title = "労働者予定一覧 | Yamori デモ"
, body =
let
name =
case role of
Worker ->
"あなた"
_ ->
"田中 太郎"
pageAction =
case editability of
Readonly ->
"閲覧"
ReadWrite ->
"閲覧と変更"
list : List (Html.Html a) -> Html.Html a
list =
Html.ul
[ style "display" "flex"
, style "flex-direction" "column"
, style "gap" "0.75em"
, style "padding" "0"
]
item : String -> PaidLeaveStatus -> Html.Html a
item date status =
Html.li
[ style "display" "flex"
, style "flex-direction" "column"
]
[ Html.span [] [ text date ]
, Html.span
[ style "font-size" "0.9em", style "opacity" "0.8" ]
[ case status of
Pending ->
text "未承認"
ManuallyApproved by ->
text (by ++ "によって承認済み")
AutoApproved ->
text "自動承認済み"
]
, Html.div
[ style "display" "flex"
, style "gap" "0.5em"
, style "margin-top" "0.25em"
]
[ case editability of
Readonly ->
text ""
ReadWrite ->
Html.button [] [ text "変更" ]
, case role of
Worker ->
text ""
_ ->
case status of
Pending ->
Html.button [] [ text "承認" ]
_ ->
Html.button [] [ text "未承認に戻す" ]
]
]
in
[ Html.h1 [] [ text "有給休暇の予定と実績" ]
, Html.p []
[ text (name ++ "の有給休暇の予定と実績の" ++ pageAction ++ "が行えます。") ]
, Html.h2 [] [ text "2026年2月" ]
, list
[ item "2026年2月3日" (ManuallyApproved "山田 花子")
, item "2026年2月10日" AutoApproved
]
, Html.h2 [] [ text "2026年3月" ]
, list
[ item "2026年3月9日" Pending
]
]
}
WorkersList _ ->
{ title = "労働者一覧 | Yamori デモ"
, body =
[ Html.h1 [] [ text "労働者の一覧" ]
, Html.p [] [ text "あなたに編集権限のある労働者の一覧です。" ]
, Html.ul
[ style "padding" "0 1em" ]
(List.map
(\worker ->
Html.li
[ style "margin" "2em 0" ]
[ Html.span [] [ text worker.displayName ]
, Html.dl
[ style "margin" "1em 0" ]
(List.foldl
(\( key, value ) nodes ->
nodes
++ [ Html.dt
[ style "font-weight" "bold"
, style "margin-top" "1em"
, style "font-size" "0.9em"
]
[ text key ]
, Html.dd
[ style "margin" "0" ]
[ text value ]
]
)
[]
worker.properties
)
, Html.div
[]
[ Html.button [] [ text "編集" ]
, Html.button [] [ text "削除" ]
]
]
)
model.workers
)
, Html.div
[ style "border" "1px solid currentColor"
, style "padding" "1em"
, style "margin" "0.5em"
]
[ Html.h2 [] [ text "デモ用メモ" ]
, Html.p []
[ Html.small []
[ text "役職や部署といった属性は \"属性名 = 値\" の自由な形式で、"
, text "それを基にサービス提供側もしくは管理者が権限管理ロジックを独立したプラグイン"
, text "として作成する。実際のところは 99% 前者が行うことになるであろうが。"
, text "これにより組織ごとに柔軟にカスタマイズされた権限管理を行うことができる。"
]
]
, Html.p []
[ Html.small []
[ text "例えば、介護事業部の労働者のみを管理できる使用者ユーザを作ったり、"
, text "事業所という属性を追加して特定の事業所に絞るといった具合。"
]
]
]
]
}
DemoIndex ->
{ title = "Yamori デモ 画面選択"
, body =
[ Html.ul []
[ Html.li [] [ Html.a [ href "/worker" ] [ text "労働者ホーム" ] ]
, Html.li [] [ Html.a [ href "/worker-mutable" ] [ text "労働者ホーム (編集権限あり)" ] ]
, Html.li [] [ Html.a [ href "/worker/schedule" ] [ text "労働者予定一覧" ] ]
, Html.li [] [ Html.a [ href "/worker/schedule-mutable" ] [ text "労働者予定一覧 (編集権限あり)" ] ]
, Html.li [] [ Html.a [ href "/employer" ] [ text "使用者ホーム" ] ]
, Html.li [] [ Html.a [ href "/employer/worker-schedule" ] [ text "使用者から見た労働者予定一覧" ] ]
, Html.li [] [ Html.a [ href "/employer/workers" ] [ text "労働者一覧(労働者はアクセス不可)" ] ]
, Html.li [] [ Html.a [ href "/admin" ] [ text "管理者ホーム" ] ]
]
]
}
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none