Files management for bookings

This commit is contained in:
Arnaud Delcasse 2022-11-01 17:06:12 +01:00
parent cb36a7c8dd
commit 72c9ca9635
12 changed files with 509 additions and 169 deletions

View File

@ -10,6 +10,7 @@ import (
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
@ -303,5 +304,8 @@ func (h ApplicationHandler) VehicleManagementBookingDisplay(w http.ResponseWrite
return
}
h.Renderer.VehicleManagementBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType())
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingid)
file_types_map := h.config.GetStringMapString("storage.files.file_types")
h.Renderer.VehicleManagementBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType(), documents, file_types_map)
}

View File

@ -3,16 +3,20 @@ package application
import (
"context"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/coreos/go-oidc"
"github.com/google/uuid"
"github.com/gorilla/mux"
@ -23,7 +27,9 @@ import (
func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var beneficiary any
var beneficiary mobilityaccountsstorage.Account
beneficiarydocuments := []filestorage.FileInfo{}
vehicles := []any{}
searched := false
@ -65,6 +71,8 @@ func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Reques
vehicles = append(vehicles, v)
}
}
beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID)
}
accounts, err := h.beneficiaries(r)
@ -76,7 +84,10 @@ func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Reques
sort.Sort(sorting.BeneficiariesByName(accounts))
h.Renderer.VehiclesSearch(w, r, accounts, searched, vehicles, beneficiary, r.FormValue("startdate"), r.FormValue("enddate"))
mandatory_documents := h.config.GetStringSlice("modules.fleets.booking_documents.mandatory")
file_types_map := h.config.GetStringMapString("storage.files.file_types")
h.Renderer.VehiclesSearch(w, r, accounts, searched, vehicles, beneficiary, r.FormValue("startdate"), r.FormValue("enddate"), mandatory_documents, file_types_map, beneficiarydocuments)
}
func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
@ -111,7 +122,7 @@ func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
vehicleid := vars["vehicleid"]
beneficiaryid := vars["beneficiaryid"]
r.ParseForm()
r.ParseMultipartForm(10 * 1024 * 1024)
start := r.FormValue("startdate")
end := r.FormValue("enddate")
@ -153,6 +164,42 @@ func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
Booking: booking,
}
fmt.Println(r.FormFile("doc-identity_proof"))
for _, v := range h.config.GetStringSlice("modules.fleets.booking_documents.mandatory") {
existing_file := r.FormValue("type-" + v)
if existing_file == "" {
file, header, err := r.FormFile("doc-" + v)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Document manquant : " + v))
return
}
defer file.Close()
fileid := uuid.NewString()
metadata := map[string]string{
"type": v,
"name": header.Filename,
}
if err := h.filestorage.Put(file, filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s_%s", booking.Id, fileid, header.Filename), header.Size, metadata); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
path := strings.Split(existing_file, "/")
if err := h.filestorage.Copy(existing_file, fmt.Sprintf("%s/%s/%s", filestorage.PREFIX_BOOKINGS, booking.Id, path[len(path)-1])); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
_, err = h.services.GRPC.Fleets.CreateBooking(context.TODO(), request)
if err != nil {
fmt.Println(err)
@ -202,7 +249,10 @@ func (h ApplicationHandler) VehicleBookingDisplay(w http.ResponseWriter, r *http
return
}
h.Renderer.VehicleBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType())
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingid)
file_types_map := h.config.GetStringMapString("storage.files.file_types")
h.Renderer.VehicleBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType(), documents, file_types_map)
}
func (h ApplicationHandler) VehiclesBookingsList(w http.ResponseWriter, r *http.Request) {
@ -222,3 +272,28 @@ func (h ApplicationHandler) VehiclesBookingsList(w http.ResponseWriter, r *http.
h.Renderer.VehicleBookingsList(w, r, bookings)
}
func (h *ApplicationHandler) BookingDocumentDownload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookingid := vars["bookingid"]
document := vars["document"]
fmt.Println(fmt.Sprintf("%s/%s", bookingid, document))
file, info, err := h.filestorage.Get(filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s", bookingid, document))
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", info.ContentType)
if _, err = io.Copy(w, file); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/vehicles/bookings/%s", bookingid), http.StatusFound)
}

View File

@ -89,12 +89,14 @@ func main() {
application.HandleFunc("/vehicles/bookings/", applicationHandler.VehiclesBookingsList)
application.HandleFunc("/vehicles/bookings/{bookingid}", applicationHandler.VehicleBookingDisplay)
application.HandleFunc("/vehicles/v/{vehicleid}/b/{beneficiaryid}", applicationHandler.Book)
application.HandleFunc("/vehicles/bookings/{bookingid}/documents/{document}", applicationHandler.BookingDocumentDownload)
application.HandleFunc("/vehicles-management/", applicationHandler.VehiclesManagementOverview)
application.HandleFunc("/vehicles-management/fleet/add", applicationHandler.VehiclesFleetAdd)
application.HandleFunc("/vehicles-management/fleet/{vehicleid}", applicationHandler.VehiclesFleetDisplay)
application.HandleFunc("/vehicles-management/fleet/{vehicleid}/update", applicationHandler.VehiclesFleetUpdate)
application.HandleFunc("/vehicles-management/bookings/", applicationHandler.VehiclesManagementBookingsList)
application.HandleFunc("/vehicles-management/bookings/{bookingid}", applicationHandler.VehicleManagementBookingDisplay)
application.HandleFunc("/vehicles-management/bookings/{bookingid}/documents/{document}", applicationHandler.BookingDocumentDownload)
application.HandleFunc("/agenda/", applicationHandler.AgendaHome)
application.HandleFunc("/agenda/create-event", applicationHandler.AgendaCreateEvent)
application.HandleFunc("/agenda/{eventid}", applicationHandler.AgendaDisplayEvent)

View File

@ -3,6 +3,7 @@ package renderer
import (
"net/http"
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
)
@ -59,14 +60,16 @@ func (renderer *Renderer) VehiclesFleetUpdate(w http.ResponseWriter, r *http.Req
renderer.Render("fleet display vehicle", w, r, files, state)
}
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any) {
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.booking_display.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
"booking": booking,
"vehicle": vehicle,
"beneficiary": beneficiary,
"group": group,
"booking": booking,
"vehicle": vehicle,
"beneficiary": beneficiary,
"group": group,
"documents": documents,
"file_types_map": file_types_map,
}
renderer.Render("vehicles search", w, r, files, state)

View File

@ -1,14 +1,28 @@
package renderer
import (
"fmt"
"net/http"
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
const vehiclesMenu = "vehicles"
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []any, beneficiary any, startdate any, enddate any) {
func selectDocumentsDefaults(beneficiarydocuments []filestorage.FileInfo, mandatory_documents []string) map[string]string {
res := map[string]string{}
for _, v := range mandatory_documents {
for _, d := range beneficiarydocuments {
if d.Metadata["Type"] == v {
res[v] = d.Key
}
}
}
return res
}
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []any, beneficiary any, startdate any, enddate any, mandatory_documents []string, file_types_map map[string]string, beneficiarydocuments []filestorage.FileInfo) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
viewstate := map[string]any{
@ -16,12 +30,18 @@ func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request,
"searched": searched,
}
fmt.Println(mandatory_documents)
if searched {
viewstate["search"] = map[string]any{
"startdate": startdate,
"enddate": enddate,
"vehicles": vehicles,
"beneficiary": beneficiary,
"startdate": startdate,
"enddate": enddate,
"vehicles": vehicles,
"beneficiary": beneficiary,
"mandatory_documents": mandatory_documents,
"file_types_map": file_types_map,
"beneficiary_documents": beneficiarydocuments,
"documents_defaults": selectDocumentsDefaults(beneficiarydocuments, mandatory_documents),
}
}
@ -30,14 +50,16 @@ func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request,
renderer.Render("vehicles search", w, r, files, state)
}
func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any) {
func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.booking_display.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
state.ViewState = map[string]any{
"booking": booking,
"vehicle": vehicle,
"beneficiary": beneficiary,
"group": group,
"booking": booking,
"vehicle": vehicle,
"beneficiary": beneficiary,
"group": group,
"documents": documents,
"file_types_map": file_types_map,
}
renderer.Render("vehicles search", w, r, files, state)

View File

@ -1,10 +1,10 @@
{{define "content"}}
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 space-y-6">
<h1 class="text-2xl font-semibold text-gray-900">Demande de support</h1>
<h1 class="text-2xl font-semibold text-gray-900">Demande de support technique</h1>
<div class="bg-white py-2 px-4 shadow sm:rounded-lg sm:px-10">
<p class="text-sm text-gray-600 p-4">
Le support PARCOURSMOB est ouvert les jours ouvrés de 9h à 18h. Vous pouvez également nous joindre par email à <b class="text-co-blue"><a href="mailto:support@parcoursmob.fr">support@parcoursmob.fr</a></b>, par exemple pour nous envoyez des copies d'écran du problème que vous rencontrez.
Le support technique PARCOURSMOB est ouvert les jours ouvrés de 9h à 18h. Vous pouvez également nous joindre par email à <b class="text-co-blue"><a href="mailto:support@parcoursmob.fr">support@parcoursmob.fr</a></b>, par exemple pour nous envoyez des copies d'écran du problème que vous rencontrez.
</p>
<form action="" method="POST">

View File

@ -76,10 +76,10 @@
<p class="mt-1 text-sm text-gray-500">Informations utiles sur la réservation.</p>
</div>
<div class="ml-4 mt-4 flex-shrink-0">
<button type="button"
<!-- <button type="button"
class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-xs font-medium rounded-2xl text-co-blue bg-gray-100 hover:bg-co-blue hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">SMS</button>
<button type="button"
class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-xs font-medium rounded-2xl text-co-blue bg-gray-100 hover:bg-co-blue hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Email</button>
class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-xs font-medium rounded-2xl text-co-blue bg-gray-100 hover:bg-co-blue hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Email</button> -->
<!-- <button type="button"
class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-xs font-medium rounded-2xl text-co-blue bg-gray-100 hover:bg-co-blue hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Imprimer</button> -->
</div>
@ -141,6 +141,55 @@
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Enddate).Format
"02/01/2006"}}</dd>
</div>
<div>
<p class="text-sm font-medium text-gray-500 my-4">Documents</p>
{{if eq (len .ViewState.documents) 0}}
<p class="p-12 text-gray-500 text-center text-md">Aucun document</p>
{{end}}
{{if gt (len .ViewState.documents) 0}}
<div class="-mx-4 mb-10 ring-1 ring-gray-300 sm:-mx-6 md:mx-0 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300 table-fixed">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Type</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Nom du document</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Ajouté le</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{{range .ViewState.documents}}
<tr>
<td class="relative py-4 pl-4 sm:pl-6 pr-3 text-sm">
<div class="font-medium text-gray-900">
<span class="bg-co-blue text-xs text-white rounded-xl p-1 mr-2">{{index $.ViewState.file_types_map .Metadata.Type}}</span>
</div>
</td>
<td class="px-3 py-3.5 text-sm text-gray-900 table-cell max-w-10 overflow-hidden">
<p class=" overflow-hidden">{{.Metadata.Name}}</p>
</td>
<td class="px-3 py-3.5 text-sm text-gray-500 lg:table-cell">{{.LastModified.Format "02/01/2006"}}</td>
<td class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right text-sm font-medium">
<a href="/app/vehicles/bookings/{{$.ViewState.booking.ID}}/documents/{{.FileName}}" target="_blank">
<button type="button" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-30">Voir<span class="sr-only"> le document</span></button>
</a>
</td>
</tr>
{{end}}
<!-- More plans... -->
</tbody>
</table>
</div>
{{end}}
</div>
</dl>
</div>
</div>

View File

@ -141,8 +141,86 @@
<td class="whitespace-nowrap py-4 px-3 text-sm text-gray-500">{{.Data.licence_plate}}</td>
<td class="whitespace-nowrap py-4 px-3 text-sm text-gray-500">COOPGO</td>
<td class="whitespace-nowrap py-4 px-3 text-sm text-gray-500">{{if .Data.address}}{{.Data.address.properties.label}}{{end}}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0">
<a href="/app/vehicles/v/{{.ID}}/b/{{$.ViewState.search.beneficiary.ID}}?startdate={{$.ViewState.search.startdate}}&enddate={{$.ViewState.search.enddate}}" class="text-co-blue hover:text-co-blue">Réserver<span class="sr-only"> pour {{$.ViewState.search.beneficiary.Data.first_name}} {{$.ViewState.search.beneficiary.Data.last_name}}</span></a>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
x-data="{
documentsdialog: false
}">
<!-- <a href="/app/vehicles/v/{{.ID}}/b/{{$.ViewState.search.beneficiary.ID}}?startdate={{$.ViewState.search.startdate}}&enddate={{$.ViewState.search.enddate}}" class="text-co-blue hover:text-co-blue">Réserver<span class="sr-only"> pour {{$.ViewState.search.beneficiary.Data.first_name}} {{$.ViewState.search.beneficiary.Data.last_name}}</span></a> -->
<a href="#" @click="documentsdialog = !documentsdialog" class="text-co-blue hover:text-co-blue">Réserver<span class="sr-only"> pour {{$.ViewState.search.beneficiary.Data.first_name}} {{$.ViewState.search.beneficiary.Data.last_name}}</span></a>
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true"
x-show="documentsdialog">
<!--
Background backdrop, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 bg-gray-900 bg-opacity-30 transition-opacity"></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
-->
<div class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-co bg-co-blue">
<!-- Heroicon name: outline/check -->
{{$.IconSet.Icon "hero:outline/folder-plus" "h-6 w-6 text-white"}}
<!-- <svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg> -->
</div>
<div class="mt-3 text-center sm:mt-5">
<h3 class="text-lg font-medium leading-6 text-gray-900" id="modal-title">Documents demandés</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">Ajoutez des documents pour finaliser</p>
</div>
</div>
</div>
<form enctype="multipart/form-data" method="POST" action="/app/vehicles/v/{{.ID}}/b/{{$.ViewState.search.beneficiary.ID}}?startdate={{$.ViewState.search.startdate}}&enddate={{$.ViewState.search.enddate}}">
{{range $.ViewState.search.mandatory_documents}}
{{$type := .}}
<div class="p-2"
x-data="{
select: '{{index $.ViewState.search.documents_defaults $type}}'
}">
<label for="type" class="block text-sm font-medium text-gray-700">{{index $.ViewState.search.file_types_map $type}}</label>
<select x-model="select" id="type-{{$type}}" name="type-{{$type}}" class="mt-1 block w-full rounded-2xl border-gray-300 py-2 pl-3 pr-10 text-base focus:border-co-blue focus:outline-none focus:ring-co-blue sm:text-sm">
{{range $.ViewState.search.beneficiary_documents}}
{{if eq $type .Metadata.Type}}
<option value="{{.Key}}">Fichier bénéficiaire : {{.Metadata.Name}}</option>
{{end}}
{{end}}
<option value="">Ajouter un fichier</option>
</select>
<div x-show="select == ''" class="p-2">
<input type="file" name="doc-{{$type}}" />
</div>
</div>
{{end}}
<div class="mt-5 sm:mt-6">
<button type="submit" class="inline-flex w-full justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:text-sm">Réserver</button>
</div>
</form>
<div class="mt-5 sm:mt-6">
<button @click="documentsdialog=false" type="button" class="inline-flex w-full justify-center max-w-xs bg-white hover:bg-gray-50 border-gray-300 border px-4 py-2 text-gray-700 items-center text-sm rounded-2xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Annuler</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
{{end}}

View File

@ -216,6 +216,55 @@
</button>
</dd>
</div>
<div>
<p class="text-sm font-medium text-gray-500 my-4">Documents</p>
{{if eq (len .ViewState.documents) 0}}
<p class="p-12 text-gray-500 text-center text-md">Aucun document</p>
{{end}}
{{if gt (len .ViewState.documents) 0}}
<div class="-mx-4 mb-10 ring-1 ring-gray-300 sm:-mx-6 md:mx-0 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300 table-fixed">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Type</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Nom du document</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Ajouté le</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{{range .ViewState.documents}}
<tr>
<td class="relative py-4 pl-4 sm:pl-6 pr-3 text-sm">
<div class="font-medium text-gray-900">
<span class="bg-co-blue text-xs text-white rounded-xl p-1 mr-2">{{index $.ViewState.file_types_map .Metadata.Type}}</span>
</div>
</td>
<td class="px-3 py-3.5 text-sm text-gray-900 table-cell max-w-10 overflow-hidden">
<p class=" overflow-hidden">{{.Metadata.Name}}</p>
</td>
<td class="px-3 py-3.5 text-sm text-gray-500 lg:table-cell">{{.LastModified.Format "02/01/2006"}}</td>
<td class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right text-sm font-medium">
<a href="/app/vehicles/bookings/{{$.ViewState.booking.ID}}/documents/{{.FileName}}" target="_blank">
<button type="button" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-30">Voir<span class="sr-only"> le document</span></button>
</a>
</td>
</tr>
{{end}}
<!-- More plans... -->
</tbody>
</table>
</div>
{{end}}
</div>
</dl>
</div>
</div>

View File

@ -806,18 +806,6 @@ html {
left: 1rem;
}
.left-6 {
left: 1.5rem;
}
.-top-px {
top: -1px;
}
.right-6 {
right: 1.5rem;
}
.z-40 {
z-index: 40;
}
@ -850,6 +838,15 @@ html {
margin: 0.5rem;
}
.m-4 {
margin: 1rem;
}
.-mx-4 {
margin-left: -1rem;
margin-right: -1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@ -860,11 +857,6 @@ html {
margin-bottom: -0.5rem;
}
.-mx-4 {
margin-left: -1rem;
margin-right: -1rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
@ -913,6 +905,14 @@ html {
margin-top: 0.25rem;
}
.mt-10 {
margin-top: 2.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mr-4 {
margin-right: 1rem;
}
@ -933,10 +933,6 @@ html {
margin-top: 0.75rem;
}
.mt-10 {
margin-top: 2.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
@ -961,10 +957,6 @@ html {
margin-bottom: 1rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.ml-6 {
margin-left: 1.5rem;
}
@ -981,6 +973,10 @@ html {
margin-top: -1rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.-mt-2 {
margin-top: -0.5rem;
}
@ -1005,10 +1001,6 @@ html {
margin-right: -0.25rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.block {
display: block;
}
@ -1033,6 +1025,10 @@ html {
display: table;
}
.table-cell {
display: table-cell;
}
.flow-root {
display: flow-root;
}
@ -1085,10 +1081,6 @@ html {
height: 0.75rem;
}
.h-px {
height: 1px;
}
.max-h-60 {
max-height: 15rem;
}
@ -1157,14 +1149,18 @@ html {
width: 0px;
}
.min-w-0 {
min-width: 0px;
.w-20 {
width: 5rem;
}
.min-w-full {
min-width: 100%;
}
.min-w-0 {
min-width: 0px;
}
.max-w-xs {
max-width: 20rem;
}
@ -1185,6 +1181,14 @@ html {
max-width: 32rem;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-full {
max-width: 100%;
}
.flex-1 {
flex: 1 1 0%;
}
@ -1209,6 +1213,10 @@ html {
flex-grow: 1;
}
.table-fixed {
table-layout: fixed;
}
.origin-top-right {
transform-origin: top right;
}
@ -1228,6 +1236,16 @@ html {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-4 {
--tw-translate-y: 1rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-0 {
--tw-translate-y: 0px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.scale-95 {
--tw-scale-x: .95;
--tw-scale-y: .95;
@ -1309,6 +1327,10 @@ html {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
@ -1428,16 +1450,16 @@ html {
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-divide-opacity));
}
.divide-gray-300 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-divide-opacity));
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-divide-opacity));
}
.overflow-auto {
overflow: auto;
}
@ -1531,11 +1553,6 @@ html {
border-bottom-left-radius: 1rem;
}
.rounded-t-lg {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.border {
border-width: 1px;
}
@ -1674,6 +1691,10 @@ html {
--tw-bg-opacity: 0.75;
}
.bg-opacity-30 {
--tw-bg-opacity: 0.3;
}
.stroke-gray-800 {
stroke: #1f2937;
}
@ -1682,14 +1703,14 @@ html {
stroke: #fff;
}
.p-12 {
padding: 3rem;
}
.p-1 {
padding: 0.25rem;
}
.p-12 {
padding: 3rem;
}
.p-2 {
padding: 0.5rem;
}
@ -1698,18 +1719,14 @@ html {
padding: 1rem;
}
.p-1\.5 {
padding: 0.375rem;
}
.p-8 {
padding: 2rem;
}
.p-0 {
padding: 0px;
}
.p-1\.5 {
padding: 0.375rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
@ -1730,6 +1747,26 @@ html {
padding-bottom: 1.5rem;
}
.py-3\.5 {
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -1745,31 +1782,11 @@ html {
padding-bottom: 2.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.py-3\.5 {
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
@ -1813,20 +1830,20 @@ html {
padding-right: 2.25rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pr-12 {
padding-right: 3rem;
.pl-4 {
padding-left: 1rem;
}
.pr-4 {
padding-right: 1rem;
}
.pl-4 {
padding-left: 1rem;
.pr-2 {
padding-right: 0.5rem;
}
.pr-12 {
padding-right: 3rem;
}
.pl-1 {
@ -1837,10 +1854,18 @@ html {
padding-right: 2.5rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pt-8 {
padding-top: 2rem;
}
.pb-6 {
padding-bottom: 1.5rem;
}
.pt-4 {
padding-top: 1rem;
}
@ -1865,14 +1890,6 @@ html {
padding-left: 0.375rem;
}
.pb-6 {
padding-bottom: 1.5rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.text-left {
text-align: left;
}
@ -1899,6 +1916,11 @@ html {
line-height: 1.5rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
@ -1909,11 +1931,6 @@ html {
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
@ -1952,14 +1969,14 @@ html {
text-transform: capitalize;
}
.leading-6 {
line-height: 1.5rem;
}
.leading-4 {
line-height: 1rem;
}
.leading-6 {
line-height: 1.5rem;
}
.leading-5 {
line-height: 1.25rem;
}
@ -2017,16 +2034,16 @@ html {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-co-green {
--tw-text-opacity: 1;
color: rgb(108 193 31 / var(--tw-text-opacity));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-co-green {
--tw-text-opacity: 1;
color: rgb(108 193 31 / var(--tw-text-opacity));
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
@ -2094,6 +2111,12 @@ html {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-xl {
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.ring-1 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -2123,16 +2146,16 @@ html {
--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
}
.ring-white {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity));
}
.ring-gray-300 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
}
.ring-white {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity));
}
.ring-opacity-5 {
--tw-ring-opacity: 0.05;
}
@ -2168,6 +2191,12 @@ html {
transition-duration: 150ms;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
@ -2235,11 +2264,6 @@ html {
--tw-ring-color: rgb(36 56 135 / var(--tw-ring-opacity));
}
.focus-within\:ring-indigo-500:focus-within {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
}
.focus-within\:ring-offset-2:focus-within {
--tw-ring-offset-width: 2px;
}
@ -2317,11 +2341,6 @@ html {
color: inherit;
}
.hover\:text-indigo-500:hover {
--tw-text-opacity: 1;
color: rgb(99 102 241 / var(--tw-text-opacity));
}
.focus\:border-transparent:focus {
border-color: transparent;
}
@ -2341,11 +2360,6 @@ html {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.focus\:border-indigo-600:focus {
--tw-border-opacity: 1;
border-color: rgb(79 70 229 / var(--tw-border-opacity));
}
.focus\:placeholder-gray-400:focus::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
@ -2482,6 +2496,11 @@ html {
margin-right: auto;
}
.sm\:my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.sm\:mt-0 {
margin-top: 0px;
}
@ -2494,12 +2513,16 @@ html {
margin-left: 1.25rem;
}
.sm\:block {
display: block;
.sm\:mt-5 {
margin-top: 1.25rem;
}
.sm\:inline {
display: inline;
.sm\:mt-6 {
margin-top: 1.5rem;
}
.sm\:block {
display: block;
}
.sm\:flex {
@ -2530,6 +2553,10 @@ html {
max-width: 28rem;
}
.sm\:max-w-sm {
max-width: 24rem;
}
.sm\:flex-auto {
flex: 1 1 auto;
}
@ -2542,6 +2569,23 @@ html {
flex: 1 1 0%;
}
.sm\:translate-y-0 {
--tw-translate-y: 0px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:scale-95 {
--tw-scale-x: .95;
--tw-scale-y: .95;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:scale-100 {
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@ -2625,6 +2669,10 @@ html {
padding: 1.5rem;
}
.sm\:p-0 {
padding: 0px;
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
@ -2676,14 +2724,6 @@ html {
grid-column: span 2 / span 2;
}
.md\:col-span-3 {
grid-column: span 3 / span 3;
}
.md\:col-span-6 {
grid-column: span 6 / span 6;
}
.md\:mx-0 {
margin-left: 0px;
margin-right: 0px;
@ -2826,10 +2866,6 @@ html {
display: table-cell;
}
.lg\:hidden {
display: none;
}
.lg\:max-w-7xl {
max-width: 80rem;
}

View File

@ -9,6 +9,7 @@ import (
const (
PREFIX_BENEFICIARIES = "beneficiaries"
PREFIX_BOOKINGS = "fleets_bookings"
)
type FileInfo struct {
@ -23,6 +24,7 @@ type FileStorage interface {
Put(reader io.Reader, prefix string, filename string, size int64, metadata map[string]string) error
List(prefix string) []FileInfo
Get(prefix string, file string) (io.Reader, *FileInfo, error)
Copy(src string, dest string) error
}
func NewFileStorage(cfg *viper.Viper) (FileStorage, error) {

View File

@ -102,3 +102,23 @@ func (s *MinioStorageHandler) Get(prefix string, file string) (io.Reader, *FileI
return object, fileinfo, nil
}
func (s *MinioStorageHandler) Copy(src string, dst string) error {
srcOpts := minio.CopySrcOptions{
Bucket: s.BucketName,
Object: src,
}
// Destination object
dstOpts := minio.CopyDestOptions{
Bucket: s.BucketName,
Object: dst,
}
_, err := s.Client.CopyObject(context.Background(), dstOpts, srcOpts)
if err != nil {
fmt.Println(err)
return err
}
return nil
}