added address control to customer profile

added comment list
added product favorites control
master
A1Gard 1 month ago
parent 1eda1fe3c6
commit 5437e5f44b

@ -2,6 +2,8 @@
namespace App\Http\Controllers;
use App\Models\Address;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Product;
use Illuminate\Http\Request;
@ -9,6 +11,18 @@ use Illuminate\Validation\Rules\In;
class CustomerController extends Controller
{
public function addressSave(Address $address, Request $request)
{
$address->address = $request->input('address');
$address->lat = $request->input('lat');
$address->lng = $request->input('lng');
$address->state_id = $request->input('state_id')??null;
$address->city_id = $request->input('city_id')??null;
$address->zip = $request->input('zip');
$address->save();
return $address;
}
//
public function __construct()
{
@ -93,5 +107,65 @@ class CustomerController extends Controller
}
public function addresses(){
return auth('customer')->user()->addresses;
}
public function addressUpdate(Request $request, $item)
{
$item = Address::where('id', $item)->firstOrFail();
if ($item->customer_id != auth('customer')->user()->id) {
return abort(403);
}
//
$request->validate([
'address' => ['required', 'string', 'min:10'],
'zip' => ['required', 'string', 'min:5'],
'state_id' => ['required', 'exists:states,id'],
'city_id' => ['required', 'exists:cities,id'],
'lat' => ['nullable'],
'lng' => ['nullable'],
]);
$this->addressSave($item, $request);
return ['OK' => true, "message" => __("address updated")];
}
/**
* Remove the specified resource from storage.
*/
public function addressDestroy(Address $item)
{
//
if ($item->customer_id != auth('customer')->id()) {
return abort(403);
}
$add = $item->address ;
$item->delete();
return ['OK' => true, "message" => __(":ADDRESS removed",['ADDRESS' => $add])];
}
public function addressStore(Request $request)
{
//
$request->validate([
'address' => ['required', 'string', 'min:10'],
'zip' => ['required', 'string', 'min:5'],
'state_id' => ['required', 'exists:states,id'],
'city_id' => ['required', 'exists:cities,id'],
'lat' => ['nullable'],
'lng' => ['nullable'],
]);
$address = new Address();
$address->customer_id = auth('customer')->user()->id;
$address = $this->addressSave($address, $request);
return ['OK' => true,'message' => __("Address added successfully"), 'list'=> auth('customer')->user()->addresses];
}
}

@ -49,4 +49,9 @@ class Customer extends Authenticatable
return $this->belongsToMany(Product::class,'customer_product');
}
public function comments(){
return $this->morphMany(Comment::class, 'commentator');
}
}

@ -22,6 +22,9 @@ app.component('mp4player', videoPlayer);
import mp3player from "../client-vue/mp3player.vue";
app.component('mp3player', mp3player);
import addressInput from "../client-vue/AddressInput.vue";
app.component('address-input', addressInput);
import NsCard from "../client-vue/NsCard.vue";
app.component('ns-card', NsCard);

@ -0,0 +1,427 @@
<template>
<div id="address-input">
<ul class="list-group mb-2">
<li class="list-group-item" v-for="ad in addresses">
<div class="btn btn-outline-danger btn-sm float-end mx-2" @click="removing(ad.id)">
<i class="ri-close-line"></i>
</div>
<div class="btn btn-outline-primary btn-sm float-end" @click="editing(ad)">
<i class="ri-edit-2-line"></i>
</div>
<div class="p-2">
{{ ad.address }}
</div>
</li>
</ul>
<button type="button" class="btn btn-primary" @click="adding">
<i class="ri-add-line"></i>
</button>
<div id="address-modal" v-if="modal" @click.self="modal = false">
<div class="card">
<div class="card-header">
{{ translate['addr-editor'] }}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<label for="st">
{{ translate['state'] }} :
</label>
<select @change="updateState" class="form-control" v-model="state_id" id="st">
<option :data-lat="s.lat" :data-lng="s.lng" :value="s.id" v-for="s in states">
{{ s.name }}
</option>
</select>
</div>
<div class="col-md-6">
<label for="st">
{{ translate['city'] }}:
</label>
<select @change="updateCity" class="form-control" v-model="city_id" id="st">
<option :value="c.id" v-for="c in cities"> {{ c.name }}</option>
</select>
</div>
<div class="col-12 my-3">
<div ref="mapContainer" :style="'height: 300px;'+mapStyle"></div>
</div>
<div class="col-12">
<textarea rows="2" class="form-control" :placeholder="translate['address']"
v-model="address"></textarea>
</div>
<div class="col-12">
<label for="zip">
{{ translate['post-code'] }}:
</label>
<input type="text" class="form-control" v-model="zip">
</div>
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary w-100" type="button" @click="save">
<i class="ri-save-2-line"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import {useToast} from 'vue-toast-notification';
import axios from "axios";
const $toast = useToast();
export default {
name: "address-input",
components: {},
data: () => {
return {
id: null,
action: 'add',
modal: false,
addresses: [],
states: [],
cities: [],
state_id: null,
city_id: null,
map: null,
marker: null,
zoom: 10,
address: '',
zip: '',
lat: null,
lng: null,
}
},
props: {
listLink: {
type: String,
required: true,
},
addLink: {
type: String,
required: true,
},
updateLink: {
type: String,
required: true,
},
remLink: {
type: String,
required: true,
},
stateLink: {
type: String,
required: true,
},
citiesLink: {
type: String,
required: true,
},
darkMode: {
type: Boolean,
default: false,
},
translate: {
default: {},
}
},
async mounted() {
try {
let res = await axios.get(this.stateLink);
this.states = res.data.data;
// console.log(res.data);
} catch (e) {
$toast.error(e.message);
}
await this.updateList();
// await this.initMap();
// if (this.states[0].lat != null && this.states[0].lng != null){
// this.changeMapCenter(this.states[0].lat,this.states[0].lng)
// }
},
computed: {
mapStyle() {
if (this.darkMode) {
return 'filter: invert(100%) hue-rotate(120deg) brightness(95%) contrast(90%);';
}
return '';
}
},
methods: {
async save() {
let canSave = true;
if (this.state_id == null) {
$toast.error("State is required"); // WIP translate
canSave = false;
}
if (this.city_id == null) {
$toast.error("City is required"); // WIP translate
canSave = false;
}
if (this.address.length < 10) {
$toast.error("Address is required"); // WIP translate
canSave = false;
}
if (this.zip.length < 5) {
$toast.error("Post code is required"); // WIP translate
canSave = false;
}
if (!canSave) {
return false;
}
if (this.action == 'add') {
let data = {
address: this.address,
state_id: this.state_id,
city_id: this.city_id,
zip: this.zip,
lat: this.lat,
lng: this.lng
};
try {
let r = await axios.post(this.addLink, data);
if (r.data.OK) {
this.addresses = r.data.list;
$toast.success(r.data.message);
this.modal = false;
}
} catch (e) {
$toast.error('err!' + e.message);
}
} else {
let data = {
address: this.address,
state_id: this.state_id,
city_id: this.city_id,
zip: this.zip,
lat: this.lat,
lng: this.lng
};
try {
const url = this.updateLink + '/' + this.id;
let r = await axios.post(url, data);
if (r.data.OK) {
$toast.success(r.data.message);
await this.updateList();
this.modal = false;
}
} catch (e) {
$toast.error('err!' + e.message);
}
}
},
showModal() {
this.modal = true;
setTimeout(() => {
this.initMap();
}, 50);
},
async removing(id) {
if (!confirm('Sure?')) { //WIP: translate
return;
}
const url = this.remLink + '/' + id;
try {
let r = await axios.get(url);
if (r.data.OK) {
$toast.success(r.data.message);
this.updateList();
}
} catch (e) {
$toast.error('err!' + e.message);
}
},
async editing(dt) {
this.showModal();
this.action = 'edit';
this.id = dt.id;
this.lat = dt.lat;
this.lng = dt.lng;
this.zip = dt.zip;
this.address = dt.address;
this.state_id = dt.state_id;
await this.updateState();
this.city_id = dt.city_id;
if (this.lng != null && this.lat != null) {
this.zoom = 16;
setTimeout(() => {
this.changeMapCenter(this.lat, this.lng);
this.marker = L.marker({lat: this.lat, lng: this.lng}).addTo(this.map);
}, 100);
}
},
adding() {
this.action = 'add';
this.address = '';
this.zip = '';
this.state_id = null;
this.showModal();
},
initMap() {
this.map = L.map(this.$refs.mapContainer).setView([35.83266000, 50.99155000], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; openstreetmap',
attributionControl: false,
}).addTo(this.map);
this.map.on('click', this.onMapClick);
this.map.attributionControl.setPrefix('xShop');
},
onMapClick(e) {
if (this.marker) {
this.map.removeLayer(this.marker);
}
this.marker = L.marker(e.latlng).addTo(this.map);
// You can emit the selected location or perform any other desired action here
// console.log('Selected location:', e.latlng);
this.getAddress(e.latlng);
this.lat = e.latlng.lat;
this.lng = e.latlng.lng;
},
changeMapCenter(lat, lng) {
try {
this.map.setView([lat, lng], this.zoom);
} catch (e) {
// console.log(e.message);
setTimeout(() => {
console.log('repeat');
this.changeMapCenter(lat, lng);
}, 10);
}
// Change the map center to [40.7128, -74.0059] (New York City) with zoom level 12
},
async updateList() {
try {
let res = await axios.get(this.listLink);
this.addresses = res.data;
} catch (e) {
$toast.error('err!' + e.message);
}
},
async updateState() {
for (const st of this.states) {
if (st.id == this.state_id) {
// console.log(st);
if (st.lat != null && st.lng != null) {
this.zoom = 10;
this.changeMapCenter(st.lat, st.lng)
}
break;
}
}
try {
let res = await axios.get(this.citiesLink + '/' + this.state_id);
this.cities = res.data.data;
} catch (e) {
$toast.error('err!' + e.message);
}
},
async updateCity() {
for (const c of this.cities) {
if (c.id == this.city_id) {
if (c.lat != null && c.lng != null) {
this.zoom = 12;
this.changeMapCenter(c.lat, c.lng)
}
break;
}
}
},
getAddress(latlng) {
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${latlng.lat}&lon=${latlng.lng}&addressdetails=1&accept-language=en`;
fetch(url)
.then(response => response.json())
.then(data => {
const address = this.formatAddress(data.address);
this.address = address;
})
.catch(error => {
$toast.error('err!' + error.message);
});
},
formatAddress(addressData) {
let formattedAddress = '';
if (addressData.road) {
formattedAddress += addressData.road;
}
if (addressData.neighbourhood) {
formattedAddress += addressData.neighbourhood;
}
//
// if (addressData.house_number) {
// formattedAddress += ` ${addressData.house_number}`;
// }
//
if (addressData.postcode) {
// formattedAddress += `, ${addressData.postcode}`;
let x = addressData.postcode.split('-');
this.zip = x.join('');
}
if (addressData.city) {
formattedAddress += `, ${addressData.city}`;
}
//
// if (addressData.country) {
// formattedAddress += `, ${addressData.country}`;
// }
return formattedAddress;
},
}
}
</script>
<style scoped>
#address-input {
padding: 0 .75rem 1rem;
}
#address-modal {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
backdrop-filter: blur(4px);
background: #ffffff33;
z-index: 9;
display: flex;
align-items: center;
justify-content: center;
}
#address-modal .card {
max-width: 900px;
width: 100%;
}
</style>

@ -291,7 +291,9 @@
"Previous": "قبلی",
"Price": "مبلغ",
"Product": "محصول",
"Product added to compare": "محصول به فهرست مقایسه افزوده شد",
"Product added to favorites": "محصول به علاقه‌مندی شما افزوده شد",
"Product removed from compare": "محصول از فهرست مقایسه حذف شد",
"Product removed from favorites": "محصول از علاقه مندی های شما حذف شد",
"Product table": "جدول محصول",
"Products": "محصولات",

@ -43,7 +43,7 @@
</li>
<li>
<a href="#tickets">
<i class="ri-inbox-2-line"></i>
<i class="ri-customer-service-fill"></i>
{{__("Tickets")}}
</a>
</li>
@ -105,7 +105,7 @@
</div>
<div class="avisa-grid col-lg-3 col-md-6">
<div class="grid-item">
<i class="ri-message-3-line"></i>
<i class="ri-customer-service-2-line"></i>
<h2>
{{number_format(auth('customer')->user()->tickets()->count())}}
</h2>
@ -125,6 +125,28 @@
</h3>
</div>
</div>
<div class="avisa-grid col-md-6">
<div class="grid-item">
<i class="ri-message-3-line"></i>
<h2>
{{number_format(auth('customer')->user()->comments()->count())}}
</h2>
<h3>
{{__("Comments")}}
</h3>
</div>
</div>
<div class="avisa-grid col-md-6">
<div class="grid-item">
<i class="ri-hearts-line"></i>
<h2>
{{number_format(auth('customer')->user()->favorites()->count())}}
</h2>
<h3>
{{__("Favorites")}}
</h3>
</div>
</div>
</div>
@if(cardCount() > 0)
<div class="alert alert-info mt-4">
@ -332,16 +354,73 @@
</div>
<div class="tab" id="comments">
{{-- WIP comments--}}
@if(auth('customer')->user()->comments()->count() == 0)
<div class="alert alert-info">
{{__("You don't have any comments, We are so pleased to hear your look-out")}}
</div>
@else
@foreach(auth('customer')->user()->comments as $comment)
<div class="avisa-comment">
<h3>
{{$comment->commentable->title}}
{{$comment->commentable->name}}
</h3>
<span class="comment-date float-end">
{{$comment->created_at->ldate('Y-m-d')}}
</span>
<p>
{{$comment->body}}
</p>
</div>
@endforeach
@endif
</div>
<div class="tab" id="submit-ticket">
{{-- WIP submit new ticket --}}
</div>
<div class="tab" id="addresses">
{{-- WIP submit new ticket --}}
<address-input
list-link="{{route('client.addresses')}}"
add-link="{{route('client.address.store')}}"
update-link="{{route('client.address.update','')}}"
rem-link="{{route('client.address.destroy','')}}"
state-link="{{route('v1.state.index')}}"
cities-link="{{route('v1.state.show','')}}"
:dark-mode="false"
:translate='{{vueTranslate([
'addr-editor' => __('Address editor'),
'state' => __('State'),
'city' => __('City'),
'address' => __('Address'),
'post-code' => __('Post code'),
])}}'
></address-input>
</div>
<div class="tab" id="favs">
{{-- WIP submit new ticket --}}
@foreach(auth('customer')->user()->favorites as $fav)
<div class="product-item">
<div class="row">
<div class="col-md-2">
<img src="{{$fav->imgUrl()}}" class="img-fluid" alt="{{$fav->name}}">
</div>
<div class="col-md-10">
<h4>
{{$fav->name}}
</h4>
<p class="text-muted">
{{$fav->excerpt}}
</p>
<a class="fav-btn float-end mx-2" data-slug="{{$fav->slug}}" data-is-fav="{{$fav->isFav()}}"
data-bs-custom-class="custom-tooltip"
data-bs-toggle="tooltip" data-bs-placement="top" title="{{__("Add to / Remove from favorites")}}">
<i class="ri-heart-line"></i>
<i class="ri-heart-fill"></i>
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>

@ -35,15 +35,15 @@
}
}
i{
i {
margin: 0 1rem;
}
}
}
.avisa-grid{
.grid-item{
.avisa-grid {
.grid-item {
overflow: hidden;
height: 200px;
border-radius: var(--xshop-border-radius);
@ -51,8 +51,9 @@
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
i{
i {
position: absolute;
transform: rotateZ(-17deg);
inset-inline-start: 7%;
@ -61,7 +62,7 @@
opacity: .3;
}
h3{
h3 {
position: absolute;
left: 0;
right: 0;
@ -71,27 +72,101 @@
}
}
&:nth-child(1){
.grid-item{
background: rgba(248, 170, 0, 0.63);
&:nth-child(1) {
.grid-item {
background: rgba(248, 124, 0, 0.63);
}
}
&:nth-child(2){
.grid-item{
&:nth-child(2) {
.grid-item {
background: rgba(184, 11, 109, 0.63);
}
}
&:nth-child(3){
.grid-item{
&:nth-child(3) {
.grid-item {
background: rgba(11, 184, 123, 0.63);
}
}
&:nth-child(4){
.grid-item{
&:nth-child(4) {
.grid-item {
background: rgba(11, 112, 184, 0.63);
}
}
&:nth-child(5) {
.grid-item {
background: rgba(253, 237, 63, 0.83);
}
}
&:nth-child(6) {
.grid-item {
background: rgba(139, 32, 253, 0.58);
}
}
}
.avisa-comment {
border: 1px solid var(--xshop-primary);
padding: 1rem;
border-radius: var(--xshop-border-radius);
margin-bottom: 1rem;
h3 {
font-size: 17px;
}
.comment-date {
background: var(--xshop-secondary);
color: var(--xshop-diff2);
padding: 7px;
border-radius: var(--xshop-border-radius);
}
}
.product-item{
margin-bottom: 1rem;
border-radius: var(--xshop-border-radius);
border: 1px solid var(--xshop-primary);
overflow: hidden;
.fav-btn, .compare-btn {
border-radius: var(--xshop-border-radius);
background: #ffffff55;
font-size: 25px;
display: inline-block;
align-items: center;
justify-content: center;
cursor: pointer;
transition: .4s;
min-width: 100px;
text-align: center;
margin-top: -7px;
&:hover {
background: var(--xshop-primary);
color: var(--xshop-diff);
}
}
.fav-btn {
top: calc(3% + 50px);
&[data-is-fav="-1"]{
display: none;
}
&[data-is-fav="1"]{
.ri-heart-line{
display: none;
}
}
&[data-is-fav="0"]{
.ri-heart-fill{
display: none;
}
}
}
}
}

@ -16,7 +16,7 @@
{{$post->title}}
</h4>
<span>
{{$post->created_at->ldate('Y/m/d l')}}
{{$post->created_at->ldate('Y-m-d l')}}
</span>
</div>
</div>

@ -16,7 +16,7 @@
{{$post->title}}
</h4>
<span>
{{$post->created_at->ldate('Y/m/d l')}}
{{$post->created_at->ldate('Y-m-d l')}}
</span>
</div>
</div>

@ -375,6 +375,10 @@ Route::middleware([\App\Http\Middleware\VisitorCounter::class])
Route::get('/card', [\App\Http\Controllers\CardController::class, 'index'])->name('card');
Route::get('/cardClear', [\App\Http\Controllers\CardController::class, 'clearing'])->name('card.clear');
Route::get('/profile', [\App\Http\Controllers\CustomerController::class, 'profile'])->name('profile');
Route::get('/addresses', [\App\Http\Controllers\CustomerController::class, 'addresses'])->name('addresses');
Route::post('/address/store', [\App\Http\Controllers\CustomerController::class, 'addressStore'])->name('address.store');
Route::post('/address/update/{address}', [\App\Http\Controllers\CustomerController::class, 'addressUpdate'])->name('address.update');
Route::get('/address/destroy/{address}', [\App\Http\Controllers\CustomerController::class, 'addressDestroy'])->name('address.destroy');
Route::post('/profile/save', [\App\Http\Controllers\CustomerController::class, 'save'])->name('profile.save');
Route::get('/invoice/{invoice}', [\App\Http\Controllers\CustomerController::class, 'invoice'])->name('invoice');
Route::get('/products', [ClientController::class, 'products'])->name('products');

Loading…
Cancel
Save